Chrome · Workers
CVE-2026-79201
Logic Error in Workers
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Files Changed
content/browser/browsing_data/clear_site_data_handler_browsertest.cccontent/browser/devtools/service_worker_devtools_agent_host.cccontent/browser/service_worker/embedded_worker_instance.cccontent/browser/storage_partition_impl.cc
Patch
From cd4f000a79833453cf3333762f9caa74c855e485 Mon Sep 17 00:00:00 2001 From: Ming-Ying Chung <[email protected]> Date: Tue, 18 Aug 2026 22:26:38 -0700 Subject: [PATCH] Scope worker Clear-Site-Data deletions to the worker StorageKey When a service or shared worker subresource load receives a Clear-Site-Data header, the per-worker URLLoaderNetworkServiceObserver binding has no NavigationOrDocumentHandle, so StoragePartitionImpl::CalculateStorageKey() returned nullopt and the resulting BrowsingDataFilterBuilder matched the response origin's first-party partition plus every partition with that origin as top-level site, instead of only the worker's own partition. The same fetch from a document is already narrowed via RenderFrameHostImpl::CalculateStorageKey(). Plumb the worker's blink::StorageKey through CreateURLLoaderNetworkObserverForServiceOrSharedWorker() into the URLLoaderNetworkContext, and have CalculateStorageKey() derive the response origin's key from it via StorageKey::WithOrigin() for worker contexts. EmbeddedWorkerInstance, SharedWorkerHost and ServiceWorkerDevToolsAgentHost pass the key; callers that lack one (browser-initiated, WebSocket, WebTransport) keep the existing behaviour via the default nullopt. Add a browser test that registers a service worker, fetches same- and cross-origin Clear-Site-Data responses from inside it, and verifies the BrowsingDataRemover filter carries a storage key whose top-level site matches the worker's. Bug: 513836495 Change-Id: I6d6542835186a1076be47cce7fd97123d9719fe3 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8247958 Reviewed-by: Martin Šrámek <[email protected]> Commit-Queue: Ming-Ying Chung <[email protected]> Reviewed-by: Danil Somsikov <[email protected]> Reviewed-by: Mingyu Lei <[email protected]> Reviewed-by: Ben Kelly <[email protected]> Cr-Commit-Position: refs/heads/main@{#1682035} --- diff --git a/content/browser/browsing_data/clear_site_data_handler_browsertest.cc b/content/browser/browsing_data/clear_site_data_handler_browsertest.cc index 54c862ca..8354cd7 100644 --- a/content/browser/browsing_data/clear_site_data_handler_browsertest.cc +++ b/content/browser/browsing_data/clear_site_data_handler_browsertest.cc @@ -682,6 +682,103 @@ delegate()->VerifyAndClearExpectations(); } +// Verifies that when a same-origin subresource fetch initiated by a service +// worker receives a `Clear-Site-Data` header, the deletion filter is scoped to +// the worker's `blink::StorageKey` (using the worker's top-level site). +// +// The test uses `"cache"` because `"storage"` deletion unregisters and +// terminates the active service worker instance, preventing it from completing +// the fetch and reporting the result back to the page. Using `"cache"` covers +// the same `blink::StorageKey` derivation path as `"storage"` because +// `StoragePartitionImpl::CalculateStorageKey()` resolves the key independently +// of the cleared data types. Comparing this test with the cross-origin test +// below verifies that same-origin requests retain a same-site ancestor chain +// bit while cross-origin requests are scoped as cross-site under the worker's +// top-level site. +IN_PROC_BROWSER_TEST_F(ClearSiteDataHandlerBrowserTest, + ServiceWorkerSameOriginFetchUsesWorkerStorageKey) { + GURL origin1 = https_server()->GetURL("origin1.com", "/"); + + GURL url = origin1; + AddQuery(&url, "file", "worker_setup.html"); + EXPECT_TRUE(NavigateToURL(shell(), url)); + WaitForTitle(shell(), "service worker is ready"); + + const char kFetchScript[] = + "(async () => {" + " const reg = await navigator.serviceWorker.ready;" + " return new Promise(resolve => {" + " const channel = new MessageChannel();" + " channel.port1.onmessage = e => {" + " resolve(e.data.status === 200 || e.data.status === 0);" + " };" + " reg.active.postMessage({action: 'fetch', url: $1, mode: 'no-cors'}," + " [channel.port2]);" + " });" + "})();"; + + GURL same_origin_url = https_server()->GetURL("origin1.com", "/resource"); + AddQuery(&same_origin_url, "header", "\"cache\""); + + // Expect a deletion call for `origin1.com` matching the `"cache"` directive + // in the `Clear-Site-Data` response header. The deletion filter's top-level + // site partition must match the worker's top-level site (`origin1.com`). + delegate()->ExpectClearSiteDataCall( + storage_partition_config(), url::Origin::Create(origin1), + net::SchemefulSite(origin1), + /*cookies=*/false, /*storage=*/false, /*cache=*/true); + + EXPECT_TRUE( + EvalJs(shell()->web_contents(), JsReplace(kFetchScript, same_origin_url)) + .ExtractBool()); + delegate()->VerifyAndClearExpectations(); +} + +// Verifies that when a cross-origin subresource fetch initiated by a service +// worker receives a `Clear-Site-Data` header, the deletion filter is scoped to +// the target origin while preserving the worker's top-level site partition. +IN_PROC_BROWSER_TEST_F(ClearSiteDataHandlerBrowserTest, + ServiceWorkerCrossOriginFetchUsesWorkerStorageKey) { + GURL origin1 = https_server()->GetURL("origin1.com", "/"); + GURL origin2 = https_server()->GetURL("origin2.com", "/"); + + GURL url = origin1; + AddQuery(&url, "file", "worker_setup.html"); + EXPECT_TRUE(NavigateToURL(shell(), url)); + WaitForTitle(shell(), "service worker is ready"); + + const char kFetchScript[] = + "(async () => {" + " const reg = await navigator.serviceWorker.ready;" + " return new Promise(resolve => {" + " const channel = new MessageChannel();" + " channel.port1.onmessage = e => {" + " resolve(e.data.status === 200 || e.data.status === 0);" + " };" + " reg.active.postMessage({action: 'fetch', url: $1, mode: 'no-cors'}," + " [channel.port2]);" + " });" + "})();"; + + GURL cross_origin_url = https_server()->GetURL("origin2.com", "/resource"); + AddQuery(&cross_origin_url, "header", "\"storage\""); + + // Expect a deletion call for target origin `origin2.com` matching the + // `"storage"` directive in the `Clear-Site-Data` response header. Although + // the fetch target is cross-origin, the deletion filter's top-level site + // partition must remain `origin1.com` (the hosting worker's site) to preserve + // third-party partition isolation. + delegate()->ExpectClearSiteDataCall( + storage_partition_config(), url::Origin::Create(cross_origin_url), + net::SchemefulSite(origin1), + /*cookies=*/false, /*storage=*/true, /*cache=*/false); + + EXPECT_TRUE( + EvalJs(shell()->web_contents(), JsReplace(kFetchScript, cross_origin_url)) + .ExtractBool()); + delegate()->VerifyAndClearExpectations(); +} + // Tests that Clear-Site-Data is only executed on a resource fetch // if credentials are allowed in that fetch. diff --git a/content/browser/devtools/service_worker_devtools_agent_host.cc b/content/browser/devtools/service_worker_devtools_agent_host.cc index 66c6726..1518802e 100644 --- a/content/browser/devtools/service_worker_devtools_agent_host.cc +++ b/content/browser/devtools/service_worker_devtools_agent_host.cc @@ -471,7 +471,7 @@ /*dip_reporter=*/mojo::NullRemote(), static_cast<StoragePartitionImpl*>(rph->GetStoragePartition()) ->CreateURLLoaderNetworkObserverForServiceOrSharedWorker( - ToOriginatingProcessId(rph->GetID()), origin), + ToOriginatingProcessId(rph->GetID()), origin, version->key()), NetworkServiceDevToolsObserver::MakeSelfOwned(GetId()), /*client_security_state=*/nullptr, /*network_restrictions_id=*/version->network_restrictions_id(), diff --git a/content/browser/service_worker/embedded_worker_instance.cc b/content/browser/service_worker/embedded_worker_instance.cc index 89bc304..95051090 100644 --- a/content/browser/service_worker/embedded_worker_instance.cc +++ b/content/browser/service_worker/embedded_worker_instance.cc @@ -887,7 +887,7 @@ std::move(dip_reporter), static_cast<StoragePartitionImpl*>(rph->GetStoragePartition()) ->CreateURLLoaderNetworkObserverForServiceOrSharedWorker( - ToOriginatingProcessId(rph->GetID()), origin), + ToOriginatingProcessId(rph->GetID()), origin, storage_key), NetworkServiceDevToolsObserver::MakeSelfOwned(devtools_worker_token), std::move(client_security_state), network_restrictions_id, "EmbeddedWorkerInstance::CreateFactoryBundle", diff --git a/content/browser/storage_partition_impl.cc b/content/browser/storage_partition_impl.cc index d288ee2aa..d3fb23e 100644 --- a/content/browser/storage_partition_impl.cc +++ b/content/browser/storage_partition_impl.cc @@ -2682,12 +2682,13 @@ mojo::PendingRemote<network::mojom::URLLoaderNetworkServiceObserver> StoragePartitionImpl::CreateURLLoaderNetworkObserverForServiceOrSharedWorker( const network::OriginatingProcessId& process_id, - const url::Origin& worker_origin) { + const url::Origin& worker_origin, + const std::optional<blink::StorageKey>& storage_key) { mojo::PendingRemote<network::mojom::URLLoaderNetworkServiceObserver> remote; url_loader_network_observers_.Add( this, remote.InitWithNewPipeAndPassReceiver(), - URLLoaderNetworkContext::CreateForServiceOrSharedWorker(process_id, - worker_origin)); + URLLoaderNetworkContext::CreateForServiceOrSharedWorker( + process_id, worker_origin, storage_key)); return remote; } @@ -3686,8 +3687,21 @@ return std::nullopt; } - NavigationOrDocumentHandle* handle =
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/content/browser/browsing_data/clear_site_data_handler_browsertest.cc b/content/browser/browsing_data/clear_site_data_handler_browsertest.cc
index 54c862ca..8354cd7 100644
--- a/content/browser/browsing_data/clear_site_data_handler_browsertest.cc
+++ b/content/browser/browsing_data/clear_site_data_handler_browsertest.cc
@@ -682,6 +682,103 @@
delegate()->VerifyAndClearExpectations();
}
+// Verifies that when a same-origin subresource fetch initiated by a service
+// worker receives a `Clear-Site-Data` header, the deletion filter is scoped to
+// the worker's `blink::StorageKey` (using the worker's top-level site).
+//
+// The test uses `"cache"` because `"storage"` deletion unregisters and
+// terminates the active service worker instance, preventing it from completing
+// the fetch and reporting the result back to the page. Using `"cache"` covers
+// the same `blink::StorageKey` derivation path as `"storage"` because
+// `StoragePartitionImpl::CalculateStorageKey()` resolves the key independently
+// of the cleared data types. Comparing this test with the cross-origin test
+// below verifies that same-origin requests retain a same-site ancestor chain
+// bit while cross-origin requests are scoped as cross-site under the worker's
+// top-level site.
+IN_PROC_BROWSER_TEST_F(ClearSiteDataHandlerBrowserTest,
+ ServiceWorkerSameOriginFetchUsesWorkerStorageKey) {
+ GURL origin1 = https_server()->GetURL("origin1.com", "/");
+
+ GURL url = origin1;
+ AddQuery(&url, "file", "worker_setup.html");
+ EXPECT_TRUE(NavigateToURL(shell(), url));
+ WaitForTitle(shell(), "service worker is ready");
+
+ const char kFetchScript[] =
+ "(async () => {"
+ " const reg = await navigator.serviceWorker.ready;"
+ " return new Promise(resolve => {"
+ " const channel = new MessageChannel();"
+ " channel.port1.onmessage = e => {"
+ " resolve(e.data.status === 200 || e.data.status === 0);"
+ " };"
+ " reg.active.postMessage({action: 'fetch', url: $1, mode: 'no-cors'},"
+ " [channel.port2]);"
+ " });"
+ "})();";
+
+ GURL same_origin_url = https_server()->GetURL("origin1.com", "/resource");
+ AddQuery(&same_origin_url, "header", "\"cache\"");
+
+ // Expect a deletion call for `origin1.com` matching the `"cache"` directive
+ // in the `Clear-Site-Data` response header. The deletion filter's top-level
+ // site partition must match the worker's top-level site (`origin1.com`).
+ delegate()->ExpectClearSiteDataCall(
+ storage_partition_config(), url::Origin::Create(origin1),
+ net::SchemefulSite(origin1),
+ /*cookies=*/false, /*storage=*/false, /*cache=*/true);
+
+ EXPECT_TRUE(
+ EvalJs(shell()->web_contents(), JsReplace(kFetchScript, same_origin_url))
+ .ExtractBool());
+ delegate()->VerifyAndClearExpectations();
+}
+
+// Verifies that when a cross-origin subresource fetch initiated by a service
+// worker receives a `Clear-Site-Data` header, the deletion filter is scoped to
+// the target origin while preserving the worker's top-level site partition.
+IN_PROC_BROWSER_TEST_F(ClearSiteDataHandlerBrowserTest,
+ ServiceWorkerCrossOriginFetchUsesWorkerStorageKey) {
+ GURL origin1 = https_server()->GetURL("origin1.com", "/");
+ GURL origin2 = https_server()->GetURL("origin2.com", "/");
+
+ GURL url = origin1;
+ AddQuery(&url, "file", "worker_setup.html");
+ EXPECT_TRUE(NavigateToURL(shell(), url));
+ WaitForTitle(shell(), "service worker is ready");
+
+ const char kFetchScript[] =
+ "(async () => {"
+ " const reg = await navigator.serviceWorker.ready;"
+ " return new Promise(resolve => {"
+ " const channel = new MessageChannel();"
+ " channel.port1.onmessage = e => {"
+ " resolve(e.data.status === 200 || e.data.status === 0);"
+ " };"
+ " reg.active.postMessage({action: 'fetch', url: $1, mode: 'no-cors'},"
+ " [channel.port2]);"
+ " });"
+ "})();";
+
+ GURL cross_origin_url = https_server()->GetURL("origin2.com", "/resource");
+ AddQuery(&cross_origin_url, "header", "\"storage\"");
+
+ // Expect a deletion call for target origin `origin2.com` matching the
+ // `"storage"` directive in the `Clear-Site-Data` response header. Although
+ // the fetch target is cross-origin, the deletion filter's top-level site
+ // partition must remain `origin1.com` (the hosting worker's site) to preserve
+ // third-party partition isolation.
+ delegate()->ExpectClearSiteDataCall(
+ storage_partition_config(), url::Origin::Create(cross_origin_url),
+ net::SchemefulSite(origin1),
+ /*cookies=*/false, /*storage=*/true, /*cache=*/false);
+
+ EXPECT_TRUE(
+ EvalJs(shell()->web_contents(), JsReplace(kFetchScript, cross_origin_url))
+ .ExtractBool());
+ delegate()->VerifyAndClearExpectations();
+}
+
// Tests that Clear-Site-Data is only executed on a resource fetch
// if credentials are allowed in that fetch.
diff --git a/content/test/data/browsing_data/worker.js b/content/test/data/browsing_data/worker.js
index d2915648..32e1d89d 100644
--- a/content/test/data/browsing_data/worker.js
+++ b/content/test/data/browsing_data/worker.js
@@ -72,3 +72,26 @@
{ 'headers': { 'Content-Type': 'text/html' } }
));
});
+
+self.addEventListener('message', function(event) {
+ if (event.data && event.data.action === 'fetch') {
+ event.waitUntil((async () => {
+ try {
+ const res = await fetch(
+ event.data.url,
+ {credentials: 'include', mode: event.data.mode || 'cors'});
+ if (event.ports && event.ports[0]) {
+ event.ports[0].postMessage({status: res.status});
+ } else if (event.source) {
+ event.source.postMessage({status: res.status});
+ }
+ } catch (e) {
+ if (event.ports && event.ports[0]) {
+ event.ports[0].postMessage({error: e.toString()});
+ } else if (event.source) {
+ event.source.postMessage({error: e.toString()});
+ }
+ }
+ })());
+ }
+});
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page