CVE-2026-17824
Overview
Background
- ServiceWorker Static Router
- a feature letting a service worker declare routing rules so that some responses are served directly from sources (e.g. cache) without running fetch handlers.
- `IsValidStaticRouterResponse`
- a validator that checks a static-router response against cross-origin isolation policies (
CrossOriginEmbedderPolicy/COEP andDocumentIsolationPolicy/DIP) before it is committed to the client. - `ServiceWorkerContainerHostForClient`
- the browser-side host for a service worker client that holds the client’s
policy_container_policies()and the COEP/DIP reporters. - `kServiceWorkerStaticRouterOpaqueCheck`
- the feature flag gating enforcement; the validity check only blocks a response when this flag is enabled.
Root Cause Analysis
In the pre-patch code (in service_worker_main_resource_loader.cc; the enclosing method is not named in the hunk context), the entire “Block invalid responses from the static router” logic — the call to IsValidStaticRouterResponse and the CommitCompleted(net::ERR_FAILED, ...) early return — lived inside the if (service_worker_client_ && service_worker_client_->container_host()) guard. Consequently, whenever there was no service_worker_client_ or no container_host(), the validation was skipped entirely and a potentially policy-violating (e.g. opaque) response was committed to the client. The invariant that a static-router response must always be validated against COEP/DIP before commit was therefore violated on the no-container-host path.
The fix hoists the validation out of the guard: it declares default policy values (an empty CrossOriginEmbedderPolicy, null cross_origin_embedder_policy_reporter, default DocumentIsolationPolicy, null document_isolation_policy_reporter), populates them from container_host only when one exists, and then unconditionally calls IsValidStaticRouterResponse and commits ERR_FAILED on failure. This tightens enforcement so the opaque/invalid check runs on every path.
Attack Path
- Configure a static router rule A service worker registers a static-router rule that yields a response subject to COEP/DIP validation. (inferred)
- Reach the no-container-host path
Arrange main-resource loading such that
service_worker_client_or itscontainer_host()is null when the static-router response is processed, so the old nested check is skipped. - Bypass validation
With the check unreachable,
IsValidStaticRouterResponseis never called and the invalid/opaque response is committed via the normal success path, defeating the cross-origin isolation policy. (inferred) - Leverage the leaked/isolated resource The improperly admitted response undermines COEP/DIP guarantees relied on for cross-origin isolation. (inferred)
Impact Assessment
kServiceWorkerStaticRouterOpaqueCheck feature being enabled for enforcement to matter and reaching a code path where the container host is absent. The advisory states no exploited-in-the-wild status, and any concrete cross-origin data disclosure is (inferred).Files Changed
content/browser/service_worker/service_worker_main_resource_loader.cc
Audit Directions
- Enforcement nested under optional-state guardsSearch for security/validity checks placed inside
if (object && object->child())blocks; the check may be skipped whenever the optional state is absent. - Default-vs-skip on missing policy inputsWhen a validator needs COEP/DIP or similar policy inputs, confirm the code supplies safe defaults and still runs the check rather than bypassing it.
- Feature-flag-gated blockingReview paths where a
CommitCompleted(ERR_FAILED)or block is gated by a flag and a presence check, ensuring the flag alone governs enforcement and unrelated preconditions cannot disable it.
Patch
From baf7a1818eee16b05695f67c7dcb850df86f2435 Mon Sep 17 00:00:00 2001
From: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Date: Sun, 14 Jun 2026 20:43:33 -0700
Subject: [PATCH] Bugfix: This CL fixes a bug reported in crbug.com/517655543
[analysis & reasoning]
https://docs.google.com/document/d/1IBGo0-8Q3skKIHFGRzobo44J_tXou-BjSUmsMKmM7Mg
Bug: 517655543
Change-Id: If43076756ea397d2894b0e69d9c037fa81b9b3b5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7908441
Commit-Queue: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Reviewed-by: Shunya Shishido <sisidovski@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1646570}
---
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 909b5be..87250f8 100644
--- a/content/browser/service_worker/service_worker_main_resource_loader.cc
+++ b/content/browser/service_worker/service_worker_main_resource_loader.cc
@@ -927,22 +927,32 @@
cache_matcher_->cache_lookup_duration();
// Block invalid responses from the static router.
+ network::CrossOriginEmbedderPolicy cross_origin_embedder_policy;
+ network::mojom::CrossOriginEmbedderPolicyReporter*
+ cross_origin_embedder_policy_reporter = nullptr;
+ network::DocumentIsolationPolicy document_isolation_policy;
+ network::mojom::DocumentIsolationPolicyReporter*
+ document_isolation_policy_reporter = nullptr;
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;
- }
+ cross_origin_embedder_policy = container_host->policy_container_policies()
+ .cross_origin_embedder_policy;
+ cross_origin_embedder_policy_reporter =
+ container_host->cross_origin_embedder_policy_reporter().get();
+ document_isolation_policy =
+ container_host->policy_container_policies().document_isolation_policy;
+ document_isolation_policy_reporter =
+ container_host->document_isolation_policy_reporter().get();
+ }
+ if (!IsValidStaticRouterResponse(
+ resource_request_, response, cross_origin_embedder_policy,
+ cross_origin_embedder_policy_reporter, document_isolation_policy,
+ document_isolation_policy_reporter) &&
+ base::FeatureList::IsEnabled(
+ features::kServiceWorkerStaticRouterOpaqueCheck)) {
+ CommitCompleted(net::ERR_FAILED, "Invalid response from static router");
+ return;
}
}
Original Bug Report
Static Router validation bypass allows cross-origin opaque response as navigation
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A logic flaw in ServiceWorkerMainResourceLoader::DidDispatchFetchEvent potentially allows a matched Static-Router cache source response to bypass validation checks for navigation requests. Because the client’s container host is structurally uninitialized at this point in the lifecycle, the security check is skipped. This potentially allows an attacker to load and read cross-origin opaque responses in a same-origin navigation context, bypassing the Same-Origin Policy (SOP).
Affected files:
content/browser/service_worker/service_worker_main_resource_loader.cc
Estimated timestamp from git blame: 2026-04-12
Root Cause Analysis
In content/browser/service_worker/service_worker_main_resource_loader.cc, within the method ServiceWorkerMainResourceLoader::DidDispatchFetchEvent, the browser attempts to validate responses matched via the Static-Router cache source (e.g., kCache or kRaceNetworkAndCache source types). This verification ensures that matched responses are valid (e.g., preventing opaque cross-origin responses from being loaded as a main navigation document) and conform to Cross-Origin Resource Policy (CORP) constraints:
// content/browser/service_worker/service_worker_main_resource_loader.cc
if (IsMatchedRouterSourceType(
network::mojom::ServiceWorkerRouterSourceType::kCache) || ...) {
...
// Block invalid responses from the static router.
if (service_worker_client_ && service_worker_client_->container_host()) {
ServiceWorkerContainerHostForClient* container_host =
service_worker_client_->container_host();
if (!IsValidStaticRouterResponse(
resource_request_, response, ...) &&
base::FeatureList::IsEnabled(
features::kServiceWorkerStaticRouterOpaqueCheck)) {
CommitCompleted(net::ERR_FAILED, "Invalid response from static router");
return;
}
}
}
However, service_worker_client_->container_host() is structurally nullptr at this point in the lifecycle for all main resource navigations. The container_host_ is assigned exactly once within ServiceWorkerClient::CommitResponse() (content/browser/service_worker/service_worker_client.cc), which is invoked during the navigation commit phase strictly after the response has been received, processed by DidDispatchFetchEvent, and delivered to the navigation loader.
Because of this lifecycle mismatch, this safety check is bypassed and behaves as dead code for window navigations. Even though kServiceWorkerStaticRouterOpaqueCheck is enabled by default, the validation block is never entered.
Potential Security Implications
Because the browser bypasses this security check, an attacker can potentially bypass the Same-Origin Policy (SOP) and Site Isolation by serving a cached cross-origin opaque response body within a same-origin navigation context.
While Opaque Response Blocking (ORB) restricts certain response types like HTML and JSON from being cached as opaque, other sensitive cross-origin resources (such as JavaScript configuration files, CSS, images, and media) can be fully exfiltrated.
Suggested / Potential Attacker Steps
Note: These are potential steps as our tooling agent does not have the capability to execute code or run a live proof of concept.
- A user visits
https://attacker.example, which registers a Service Worker with a static routing rule matching a specific path (e.g.,/leak) with the source set tocache:self.addEventListener('install', e => { e.addRoutes([{condition:{urlPattern:{pathname:'/leak'}}, source:'cache'}]); }); - The Service Worker fetches a sensitive cross-origin resource with
{mode: 'no-cors'}and puts the resulting opaque response into Cache Storage under/leak:const r = await fetch('https://victim.example/secret_config.js', {mode:'no-cors', credentials:'include'}); const c = await caches.open('x'); await c.put(new Request('/leak'), r); - The attacker’s page embeds an iframe pointing to
https://attacker.example/leak. - The browser process intercepts the navigation request, matches the static routing rule, retrieves the opaque response from Cache Storage, and bypasses the validation gate because
container_host()isnullptr. - The document is committed at
attacker.example/leakcontaining the victim’s cross-origin response body. - The parent page reads the iframe’s document via standard DOM/JS (
iframe.contentDocument.documentElement.outerHTML), successfully exfiltrating the cross-origin data.
Suggested Fix
To remediate this issue, the browser should not rely on a fully committed container_host() to perform these checks during DidDispatchFetchEvent. Instead, the loader should fetch the client’s security policies (such as COEP and DIP) directly from the ServiceWorkerClient (which is available before commit) or perform base validation of the response type (e.g., ensuring IsValidServiceWorkerResponse is checked on the response and resource_request_ parameters) without requiring the container host to be fully initialized.
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.