Medium watch CVSS 6.5 chrome Policy Bypass 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in ServiceWorker
DescriptionInsufficient policy enforcement in ServiceWorker
ComponentServiceWorker
Sub-componentBugfix
Bug ClassPolicy Bypass
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker517655543
Fix commitbaf7a1818eee (chromium/src) +23/-13
CWECWE-284
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-28 (per vendor advisory)
Fix landed2026-06-15 (public git)
Disclosed2026-07-29
Patch gap44 days — fix public in source before the advisory

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 and DocumentIsolationPolicy/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.

Key insight
The enforcement check was nested inside the container-host presence check, so responses were left unvalidated whenever a container host was absent; the fix moves the check outside that block and validates with default policies when no host is available.

Attack Path

  1. Configure a static router rule A service worker registers a static-router rule that yields a response subject to COEP/DIP validation. (inferred)
  2. Reach the no-container-host path Arrange main-resource loading such that service_worker_client_ or its container_host() is null when the static-router response is processed, so the old nested check is skipped.
  3. Bypass validation With the check unreachable, IsValidStaticRouterResponse is never called and the invalid/opaque response is committed via the normal success path, defeating the cross-origin isolation policy. (inferred)
  4. Leverage the leaked/isolated resource The improperly admitted response undermines COEP/DIP guarantees relied on for cross-origin isolation. (inferred)

Impact Assessment

An attacker gains insufficient policy enforcement in the ServiceWorker static-router path in the browser process, allowing a static-router response to be committed without the COEP/DIP validity check when no container host is present, per the advisory. Preconditions include the 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 guards
    Search 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 inputs
    When 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 blocking
    Review 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.
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;
     }
   }
 
Loading diff…

Original Bug Report

reported by vm...@google.com

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.

  1. 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 to cache:
    self.addEventListener('install', e => {
      e.addRoutes([{condition:{urlPattern:{pathname:'/leak'}}, source:'cache'}]);
    });
    
  2. 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);
    
  3. The attacker’s page embeds an iframe pointing to https://attacker.example/leak.
  4. 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() is nullptr.
  5. The document is committed at attacker.example/leak containing the victim’s cross-origin response body.
  6. 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.

View on issue tracker