High watch ✔︎ verified CVSS 4.3 chrome Policy Bypass 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Service Worker
DescriptionInsufficient policy enforcement in Service Worker
ComponentService Worker
Bug ClassPolicy Bypass
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar — factors set by independent review by AI
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.
Independent review by AIConfirmed
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker495999481
Fix commit1cd1e8607c8e (chromium/src) +245/-0
CWECWE-693
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-25 (per vendor advisory)
Fix landed2026-04-02 (public git)
Disclosed2026-05-19
Patch gap47 days — fix public in source before the advisory

Background

Service Worker Static Routing API
lets a service worker declare routes that resolve a request from a source such as cache without running the fetch handler.
Opaque response
a network::mojom::FetchResponseType::kOpaque response from a cross-origin no-cors fetch, whose body/headers must not be exposed to the requesting origin.
`ServiceWorkerMainResourceLoader` / `ServiceWorkerSubresourceLoader`
the browser- and renderer-process loaders that commit a service-worker-produced response to a navigation or subresource.

Root Cause Analysis

When a navigation matched a static route with a cache source, the pre-fix ServiceWorkerMainResourceLoader::DidDispatchFetchEvent recorded cache timing in the IsMatchedRouterSourceType(...kCache) branch (content/browser/service_worker/service_worker_main_resource_loader.cc:910-920) and committed the cached FetchAPIResponse without checking its response_type. Per the reporter, a cross-origin no-cors response stored via cache.put() keeps its internal status/headers/body while being marked kOpaque, so serving it as a navigation response exposed cross-origin data under the attacker’s origin — the renderer-side fetch-event validation that normally blocks opaque responses was bypassed because this path commits in the loader without the equivalent check.

The fix adds a shared validator in content/common/service_worker/service_worker_resource_loader.cc: IsValidServiceWorkerResponse(request_mode, redirect_mode, response) implements Fetch spec step 3.5.6, returning false when request_mode != kNoCors and response->response_type == kOpaque (and for kError, same-origin+kCors, kOpaqueRedirect, and multi-URL redirect cases), and IsValidStaticRouterResponse wraps it with UMA. DidDispatchFetchEvent now, for a kCache matched source, calls CommitCompleted(net::ERR_FAILED, "Invalid response from static router") when the response is invalid and features::kServiceWorkerStaticRouterOpaqueCheck is enabled (diff at pre-fix :920); the same guard is added to the renderer ServiceWorkerSubresourceLoader (diff near service_worker_subresource_loader.cc:1522). This restores the invariant that an opaque cross-origin response cannot be committed as a non-no-cors navigation/subresource.

Key insight
The static-router cache path committed responses in the loader without the opaque-response check that the normal fetch-event path enforces; the fix adds a spec-based IsValidServiceWorkerResponse check that fails the load with net::ERR_FAILED for opaque responses.

Attack Path

  1. Register a cache route An attacker page’s service worker adds a static route matching a path with source: 'cache'. (inferred: route-registration API details are from the report.)
  2. Store a cross-origin opaque response The page no-cors-fetches a cross-origin resource and cache.put()s it, so the cache holds the internal status/headers/body of a kOpaque response.
  3. Navigate to the route Navigating to the matched path makes DidDispatchFetchEvent take the kCache branch; pre-fix it commits the cached response, exposing cross-origin status/headers/body under the attacker’s origin.

Impact Assessment

A web attacker could serve a cross-origin opaque response as a same-origin navigation/subresource via a static cache route, disclosing cross-origin HTTP status, headers, and certain bodies — a Same-Origin-Policy/ORB policy bypass in the browser. The advisory rates this high severity with no in-the-wild exploitation. Note the mitigation is gated behind kServiceWorkerStaticRouterOpaqueCheck, which ships disabled by default.

Changed Functions

FunctionChangeNotes
if
content/common/service_worker/service_worker_resource_loader.cc
modified

Files Changed

  • content/browser/service_worker/service_worker_browsertest.cc
  • content/browser/service_worker/service_worker_main_resource_loader.cc
  • content/common/features.cc
  • content/common/features.h
  • content/common/service_worker/service_worker_resource_loader.cc
  • content/common/service_worker/service_worker_resource_loader.h

Audit Directions

  • Parity across response paths
    When one response path (renderer fetch-event) enforces a security check, verify alternate paths (static router, cache source) that commit responses enforce the same check.
  • Opaque/type checks before commit
    Audit loaders that commit a FetchAPIResponse to confirm response_type (kOpaque, kError, kOpaqueRedirect) is validated against the request mode before exposing body/headers.
  • Flag-gated security fixes
    Track FEATURE_DISABLED_BY_DEFAULT security guards so the protection is known to be inactive until the flag is enabled.
From 1cd1e8607c8e1a4bc58f2c54448bf34857eba8a6 Mon Sep 17 00:00:00 2001
From: Yoshisto Yanagisawa <[email protected]>
Date: Wed, 01 Apr 2026 18:14:48 -0700
Subject: [PATCH] Block invalid responses for Static Router cache source

This CL implements a security fix for the Service Worker Static Routing
API. When a navigation request matches a 'cache' source and receives an
opaque response, it is now blocked with net::ERR_FAILED if the new
feature flag kServiceWorkerStaticRouterOpaqueCheck is enabled.
This also follows https://fetch.spec.whatwg.org/#concept-http-fetch
Step 3.5.6.

This is implemented in both ServiceWorkerMainResourceLoader (browser)
and ServiceWorkerSubresourceLoader (renderer) to ensure comprehensive
coverage of requests.

A new UMA ServiceWorker.StaticRouter.*.OpaqueResponse is
added to track how often this case occurs in the wild.

Bug: 495999481
Change-Id: Icd72687e6d79fd98312e70d0d9a072f75f595e8e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7703459
Reviewed-by: Shunya Shishido <[email protected]>
Commit-Queue: Yoshisato Yanagisawa <[email protected]>
Reviewed-by: Rakina Zata Amni <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1608913}
---

diff --git a/content/browser/service_worker/service_worker_browsertest.cc b/content/browser/service_worker/service_worker_browsertest.cc
index a392f654..d7b7ce3 100644
--- a/content/browser/service_worker/service_worker_browsertest.cc
+++ b/content/browser/service_worker/service_worker_browsertest.cc
@@ -36,6 +36,7 @@
 #include "base/task/sequenced_task_runner.h"
 #include "base/task/task_traits.h"
 #include "base/test/bind.h"
+#include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/with_feature_override.h"
 #include "base/time/time.h"
@@ -7465,6 +7466,13 @@
   // The result should be got from the cache, and no network access is
   // expected.
   EXPECT_EQ(0, GetRequestCount(relative_url));
+
+  // Sync histograms from the renderer process.
+  FetchHistogramsFromChildProcesses();
+
+  // Check UMA (success case should record true).
+  histogram_tester().ExpectBucketCount(
+      "ServiceWorker.StaticRouter.Subresource.ValidResponse", true, 1);
 }
 
 IN_PROC_BROWSER_TEST_F(ServiceWorkerStaticRouterBrowserTest,
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 aa64af2..67ee2206 100644
--- a/content/browser/service_worker/service_worker_main_resource_loader.cc
+++ b/content/browser/service_worker/service_worker_main_resource_loader.cc
@@ -917,6 +917,17 @@
         cache_matcher_->cache_lookup_start();
     response_head_->service_worker_router_info->cache_lookup_time =
         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 (!IsValidStaticRouterResponse(resource_request_, response) &&
+          base::FeatureList::IsEnabled(
+              features::kServiceWorkerStaticRouterOpaqueCheck)) {
+        CommitCompleted(net::ERR_FAILED, "Invalid response from static router");
+        return;
+      }
+    }
   }
 
   // Record the timing of when the fetch event is dispatched on the worker
diff --git a/content/common/features.cc b/content/common/features.cc
index 41938e3..cd9ed686 100644
--- a/content/common/features.cc
+++ b/content/common/features.cc
@@ -677,6 +677,12 @@
 BASE_FEATURE(kServiceWorkerStaticRouterRaceRequestFix2,
              base::FEATURE_ENABLED_BY_DEFAULT);
 
+// crbug.com/495999481: When this is enabled, the navigation request should be
+// blocked when it receives an opaque response from the service worker static
+// router.
+BASE_FEATURE(kServiceWorkerStaticRouterOpaqueCheck,
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 // (crbug.com/1371756): When enabled, the static routing API starts
 // ServiceWorker when the routing result of a main resource request was network
 // fallback.
diff --git a/content/common/features.h b/content/common/features.h
index cf6bf92..590eca2b 100644
--- a/content/common/features.h
+++ b/content/common/features.h
@@ -213,6 +213,7 @@
     kServiceWorkerBypassFetchHandlerBypassedHashStrings;
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kServiceWorkerSrcdocSupport);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kServiceWorkerStaticRouterRaceRequestFix2);
+CONTENT_EXPORT BASE_DECLARE_FEATURE(kServiceWorkerStaticRouterOpaqueCheck);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(
     kServiceWorkerStaticRouterStartServiceWorker);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(
diff --git a/content/common/service_worker/service_worker_resource_loader.cc b/content/common/service_worker/service_worker_resource_loader.cc
index 9c907e5e..b917f19 100644
--- a/content/common/service_worker/service_worker_resource_loader.cc
+++ b/content/common/service_worker/service_worker_resource_loader.cc
@@ -6,13 +6,77 @@
 
 #include "base/check_op.h"
 #include "base/feature_list.h"
+#include "base/metrics/histogram_functions.h"
 #include "base/metrics/histogram_macros.h"
+#include "base/strings/strcat.h"
 #include "base/trace_event/trace_event.h"
 #include "content/public/common/content_features.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/mojom/fetch_api.mojom.h"
 #include "services/network/public/mojom/service_worker_router_info.mojom-shared.h"
+#include "third_party/blink/public/mojom/fetch/fetch_api_response.mojom.h"
 #include "third_party/perfetto/include/perfetto/tracing/track_event_args.h"
 
 namespace content {
+
+// static
+bool ServiceWorkerResourceLoader::IsValidServiceWorkerResponse(
+    network::mojom::RequestMode request_mode,
+    network::mojom::RedirectMode redirect_mode,
+    const blink::mojom::FetchAPIResponsePtr& response) {
+  // This validation follows the Fetch spec.
+  // 4.4 HTTP fetch, Step 3.5.6.
+  // If one of the following is true
+  // - response’s type is "error"
+  // - request’s mode is "same-origin" and response’s type is "cors"
+  // - request’s mode is not "no-cors" and response’s type is "opaque"
+  // - request’s redirect mode is not "manual" and response’s type is
+  //   "opaqueredirect"
+  // - request’s redirect mode is not "follow" and response’s URL list has more
+  //   than one item
+  // then return a network error.
+  // Note: Existing validation functions do not fully follow the spec.
+  if (!response) {
+    return true;
+  }
+
+  if (response->response_type == network::mojom::FetchResponseType::kError) {
+    return false;
+  }
+  if (request_mode == network::mojom::RequestMode::kSameOrigin &&
+      response->response_type == network::mojom::FetchResponseType::kCors) {
+    return false;
+  }
+  if (request_mode != network::mojom::RequestMode::kNoCors &&
+      response->response_type == network::mojom::FetchResponseType::kOpaque) {
+    return false;
+  }
+  if (redirect_mode != network::mojom::RedirectMode::kManual &&
+      response->response_type ==
+          network::mojom::FetchResponseType::kOpaqueRedirect) {
+    return false;
+  }
+  if (redirect_mode != network::mojom::RedirectMode::kFollow &&
+      response->url_list.size() > 1) {
+    return false;
+  }
+
+  return true;
+}
+
+bool ServiceWorkerResourceLoader::IsValidStaticRouterResponse(
+    const network::ResourceRequest& resource_request,
+    const blink::mojom::FetchAPIResponsePtr& response) {
+  bool is_valid = IsValidServiceWorkerResponse(
+      resource_request.mode, resource_request.redirect_mode, response);
+  base::UmaHistogramBoolean(
+      base::StrCat({"ServiceWorker.StaticRouter.",
+                    IsMainResourceLoader() ? "MainResource" : "Subresource",
+                    ".ValidResponse"}),
+      is_valid);
+  return is_valid;
+}
+
 ServiceWorkerResourceLoader::ServiceWorkerResourceLoader() = default;
 ServiceWorkerResourceLoader::~ServiceWorkerResourceLoader() = default;
 
diff --git a/content/common/service_worker/service_worker_resource_loader.h b/content/common/service_worker/service_worker_resource_loader.h
index b74b9c7f..d0ec8bd 100644
--- a/content/common/service_worker/service_worker_resource_loader.h
+++ b/content/common/service_worker/service_worker_resource_loader.h
@@ -9,9 +9,19 @@
 
 #include "base/check_op.h"
 #include "content/common/content_export.h"
+#include "mojo/public/cpp/base/big_buffer.h"
 #include "services/network/public/mojom/service_worker_router_info.mojom-shared.h"
 #include "services/network/public/mojom/url_loader.mojom.h"
 #include "third_party/blink/public/common/service_worker/service_worker_router_rule.h"
+#include "third_party/blink/public/mojom/fetch/fetch_api_response.mojom-forward.h"
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/service_worker/service_worker_browsertest.cc b/content/browser/service_worker/service_worker_browsertest.cc
index a392f654..d7b7ce3 100644
--- a/content/browser/service_worker/service_worker_browsertest.cc
+++ b/content/browser/service_worker/service_worker_browsertest.cc
@@ -36,6 +36,7 @@
 #include "base/task/sequenced_task_runner.h"
 #include "base/task/task_traits.h"
 #include "base/test/bind.h"
+#include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/with_feature_override.h"
 #include "base/time/time.h"
@@ -7465,6 +7466,13 @@
   // The result should be got from the cache, and no network access is
   // expected.
   EXPECT_EQ(0, GetRequestCount(relative_url));
+
+  // Sync histograms from the renderer process.
+  FetchHistogramsFromChildProcesses();
+
+  // Check UMA (success case should record true).
+  histogram_tester().ExpectBucketCount(
+      "ServiceWorker.StaticRouter.Subresource.ValidResponse", true, 1);
 }
 
 IN_PROC_BROWSER_TEST_F(ServiceWorkerStaticRouterBrowserTest,
diff --git a/content/common/service_worker/service_worker_resource_loader_unittest.cc b/content/common/service_worker/service_worker_resource_loader_unittest.cc
new file mode 100644
index 0000000..1a4fe5c
--- /dev/null
+++ b/content/common/service_worker/service_worker_resource_loader_unittest.cc
@@ -0,0 +1,102 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/common/service_worker/service_worker_resource_loader.h"
+
+#include "services/network/public/mojom/fetch_api.mojom.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/fetch/fetch_api_response.mojom.h"
+
+namespace content {
+
+TEST(ServiceWorkerResourceLoaderTest, IsValidServiceWorkerResponse) {
+  auto request_mode = network::mojom::RequestMode::kSameOrigin;
+  auto redirect_mode = network::mojom::RedirectMode::kFollow;
+
+  // Normal response.
+  {
+    auto response = blink::mojom::FetchAPIResponse::New();
+    response->response_type = network::mojom::FetchResponseType::kDefault;
+    EXPECT_TRUE(ServiceWorkerResourceLoader::IsValidServiceWorkerResponse(
+        request_mode, redirect_mode, response));
+  }
+
+  // Error response.
+  {
+    auto response = blink::mojom::FetchAPIResponse::New();
+    response->response_type = network::mojom::FetchResponseType::kError;
+    EXPECT_FALSE(ServiceWorkerResourceLoader::IsValidServiceWorkerResponse(
+        request_mode, redirect_mode, response));
+  }
+
+  // same-origin request and cors response.
+  {
+    auto response = blink::mojom::FetchAPIResponse::New();
+    response->response_type = network::mojom::FetchResponseType::kCors;
+    EXPECT_FALSE(ServiceWorkerResourceLoader::IsValidServiceWorkerResponse(
+        network::mojom::RequestMode::kSameOrigin, redirect_mode, response));
+  }
+
+  // cross-origin request (cors) and cors response (OK).
+  {
+    auto response = blink::mojom::FetchAPIResponse::New();
+    response->response_type = network::mojom::FetchResponseType::kCors;
+    EXPECT_TRUE(ServiceWorkerResourceLoader::IsValidServiceWorkerResponse(
+        network::mojom::RequestMode::kCors, redirect_mode, response));
+  }
+
+  // cors request and opaque response.
+  {
+    auto response = blink::mojom::FetchAPIResponse::New();
+    response->response_type = network::mojom::FetchResponseType::kOpaque;
+    EXPECT_FALSE(ServiceWorkerResourceLoader::IsValidServiceWorkerResponse(
+        network::mojom::RequestMode::kCors, redirect_mode, response));
+  }
+
+  // no-cors request and opaque response (OK).
+  {
+    auto response = blink::mojom::FetchAPIResponse::New();
+    response->response_type = network::mojom::FetchResponseType::kOpaque;
+    EXPECT_TRUE(ServiceWorkerResourceLoader::IsValidServiceWorkerResponse(
+        network::mojom::RequestMode::kNoCors, redirect_mode, response));
+  }
+
+  // opaqueredirect response and manual redirect mode (OK).
+  {
+    auto response = blink::mojom::FetchAPIResponse::New();
+    response->response_type =
+        network::mojom::FetchResponseType::kOpaqueRedirect;
+    EXPECT_TRUE(ServiceWorkerResourceLoader::IsValidServiceWorkerResponse(
+        request_mode, network::mojom::RedirectMode::kManual, response));
+  }
+
+  // opaqueredirect response and follow redirect mode.
+  {
+    auto response = blink::mojom::FetchAPIResponse::New();
+    response->response_type =
+        network::mojom::FetchResponseType::kOpaqueRedirect;
+    EXPECT_FALSE(ServiceWorkerResourceLoader::IsValidServiceWorkerResponse(
+        request_mode, network::mojom::RedirectMode::kFollow, response));
+  }
+
+  // multiple URLs in URL list and follow redirect mode (OK).
+  {
+    auto response = blink::mojom::FetchAPIResponse::New();
+    response->url_list.emplace_back("http://a.test/1");
+    response->url_list.emplace_back("http://a.test/2");
+    EXPECT_TRUE(ServiceWorkerResourceLoader::IsValidServiceWorkerResponse(
+        request_mode, network::mojom::RedirectMode::kFollow, response));
+  }
+
+  // multiple URLs in URL list and manual redirect mode.
+  {
+    auto response = blink::mojom::FetchAPIResponse::New();
+    response->url_list.emplace_back("http://a.test/1");
+    response->url_list.emplace_back("http://a.test/2");
+    EXPECT_FALSE(ServiceWorkerResourceLoader::IsValidServiceWorkerResponse(
+        request_mode, network::mojom::RedirectMode::kManual, response));
+  }
+}
+
+}  // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 45de489..c2340db 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -3107,6 +3107,7 @@
     "../common/service_worker/race_network_request_simple_buffer_manager_unittest.cc",
     "../common/service_worker/race_network_request_url_loader_client_unittest.cc",
     "../common/service_worker/race_network_request_write_buffer_manager_unittest.cc",
+    "../common/service_worker/service_worker_resource_loader_unittest.cc",
     "../common/service_worker/service_worker_router_evaluator_unittest.cc",
     "../common/url_utils_unittest.cc",
     "../common/web_ui_loading_util_unittest.cc",
Loading diff…

Original Bug Report

reported by [email protected]

Information Disclosure: SW Static Routing bypasses Opaque Response validation

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: The Service Worker Static Routing API fails to validate the response type when serving from a cache source during navigation. This allows an attacker to serve opaque cross-origin responses as a navigation response, potentially leaking internal metadata and data to the attacker’s origin.

Affected files:

  • content/browser/service_worker/service_worker_cache_storage_matcher.cc
  • content/browser/service_worker/service_worker_main_resource_loader.cc
  • content/browser/service_worker/service_worker_loader_helpers.cc

Estimated timestamp from git blame: 2025-06-26

Vulnerability Detail

A logic vulnerability in the Service Worker Static Routing API allows a web attacker to potentially bypass the Same-Origin Policy (SOP) and Opaque Response Blocking (ORB) constraints for certain resource types.

In the standard Service Worker fetch event path, the renderer-side FetchRespondWithObserver enforces that kOpaque responses cannot be served to client requests (such as navigations). However, when using the Static Routing API with a cache source, this validation is bypassed because the response is handled within the browser process without equivalent checks.

When a cross-origin resource is fetched with mode: 'no-cors' and stored using cache.put(), Blink’s FetchResponseData::PopulateFetchAPIResponse recurses into the internal_response_. This means the Cache Storage actually stores the internal status code (e.g., 200), headers, and body, even though the response type is marked as kOpaque.

When a navigation matches a static route with a cache source:

  1. ServiceWorkerCacheStorageMatcher::DidMatch receives the cached FetchAPIResponse but fails to verify if the response_type is kOpaque before passing it to the loader.
  2. In ServiceWorkerMainResourceLoader::DidDispatchFetchEvent, the check status_code != 0 passes because the internal status code (e.g., 200) was retrieved from the cache.
  3. ServiceWorkerLoaderHelpers::SaveResponseInfo then copies the internal headers and status code into the navigation’s URLResponseHead without filtering.
  4. The browser commits the response body as a same-origin document on the attacker’s origin.

Impact

A malicious website can register a Service Worker that uses a static route to serve a cross-origin resource from the cache. By doing so, the attacker can potentially:

  • Perform cross-origin login detection or existence probes by reading the exact HTTP status code of a victim’s resource.
  • Exfiltrate internal HTTP headers from cross-origin requests.
  • Read the contents of JS, CSS, images, and media files from another origin (which are permitted by ORB during the initial ’no-cors’ fetch but would normally be opaque to the attacker).

While ORB strips HTML, JSON, and XML bodies during the initial network fetch, the leak of status codes and other resource types constitutes a significant information disclosure.

Potential Reproduction Steps

These are suggested steps to trigger the vulnerability:

  1. Serve a page from attacker.com with a Service Worker. The SW install handler should call event.addRoutes with a route that matches a specific path (e.g., /leak) and uses source: 'cache'.
  2. From the attacker’s page, perform a fetch: const r = await fetch('https://victim.example/api/status', {mode:'no-cors'});.
  3. Store the result: const c = await caches.open('x'); await c.put('/leak', r);.
  4. Navigate to https://attacker.com/leak (e.g., inside an iframe).
  5. The navigation will commit the victim’s internal response data under the attacker.com origin.
  6. The attacker can then observe the navigation status or use iframe.contentDocument to infer the victim’s response status, headers, and allowed bodies.

Suggested Fix

Add a check in ServiceWorkerCacheStorageMatcher::DidMatch or ServiceWorkerMainResourceLoader::DidDispatchFetchEvent to explicitly reject responses with response_type == network::mojom::FetchResponseType::kOpaque when handling a navigation request. If an opaque response is encountered, it should be treated as a network error, mirroring the behavior of the renderer-side FetchRespondWithObserver.

Evaluated with Chrome root at commit: bb48272cafb7e24c93f55ef40da398cd206ee651


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker
Links in the report