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 <mych@chromium.org>
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 <msramek@chromium.org>
Commit-Queue: Ming-Ying Chung <mych@chromium.org>
Reviewed-by: Danil Somsikov <dsv@chromium.org>
Reviewed-by: Mingyu Lei <leimy@chromium.org>
Reviewed-by: Ben Kelly <wanderview@meta.com>
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