Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ServiceWorker
DescriptionUse after free in ServiceWorker
ComponentServiceWorker
Sub-componentBlink>ServiceWorker
Bug ClassUAF
Tracker514372708
Fix commit4c827910b25d (chromium/src) +441/-20
CISA KEVNot listed
CreditedTheKng
Disclosed2026-10-20

Files Changed

  • chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappRegistry.java
  • chrome/android/javatests/src/org/chromium/chrome/browser/app/metrics/TabbedActivityLaunchCauseMetricsTest.java
  • chrome/android/javatests/src/org/chromium/chrome/browser/ServiceTabLauncherTest.java
  • chrome/browser/android/service_tab_launcher.cc
  • chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappDataStorage.java
  • chrome/android/java/src/org/chromium/chrome/browser/ServiceTabLauncher.java
  • chrome/android/javatests/BUILD.gn

Original Bug Report

reported by so...@gmail.com

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:

REPRODUCTION CASE

Empirically reproduced end-to-end on physical Android device.

Setup (3 PSL-distinct origins):

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):

  1. Visit https://likewise-eardrum-dioxide.ngrok-free.dev/setcookie -> SameSite=Strict cookie session_strict=STRICT_SECRET_XYZ set on the victim origin.

  2. Visit https://f5ffa5f220e3cc.lhr.life/stl-attacker -> attacker page registers /stl-attacker-sw.js, requests notification permission (user taps “Autoriser”), then calls registration.showNotification('🔥 TAP ME', {requireInteraction: true, tag: 'stl'}).

  3. User pulls down notification shade, taps the notification posted by attacker.lhr.life.

  4. Chrome dispatches notificationclick to the service worker. The service worker calls clients.openWindow('https://likewise-eardrum-dioxide.ngrok-free.dev/csrf?via=stl_notif').

  5. Chromium’s service_worker_client_utils.cc::OpenWindow correctly attaches initiator_origin = attacker.lhr.life to the OpenURLParams.

  6. 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_origin is not propagated to the Java side. The Java side constructs new LoadUrlParams(url) and never calls LoadUrlParams.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_XYZ is sent to ngrok-free.dev on a navigation initiated from lhr.life. SameSite=Strict cookies 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: none confirms that Chromium emits this navigation with the “user/browser initiated” fetch metadata value, not “cross-site”. none is 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

View on issue tracker