Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Browser
DescriptionMissing authorization in Browser
ComponentBrowser
Bug ClassLogic Error
Tracker513192482
Fix commitc807650ddca9 (chromium/src) +221/-35
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
if
chrome/browser/chrome_content_browser_client.cc
modified
if
chrome/browser/preloading/prefetch/search_prefetch/search_prefetch_service_browsertest.cc
modified

Files Changed

  • chrome/browser/chrome_content_browser_client.cc
  • chrome/browser/chrome_content_browser_client.h
  • chrome/browser/preloading/prefetch/search_prefetch/search_prefetch_service_browsertest.cc
From c807650ddca97469999921e6537bade99e7c9322 Mon Sep 17 00:00:00 2001
From: Shunya Shishido <[email protected]>
Date: Mon, 03 Aug 2026 22:01:22 -0700
Subject: [PATCH] Proxy Search Prefetch served via SW preload through Web Request

SearchPrefetchURLLoaderInterceptor::MaybeCreateLoader() wraps the
prefetched response handler in MaybeProxyRequestHandler() so that the
Extensions Web Request API observes the navigation-time serving of the
cached body with the navigation's tab/frame attribution.

The sibling entry point used by the service-worker stack,
ChromeContentBrowserClient::CreateURLLoaderHandlerForServiceWorkerInitiatedNavigationRequest,
returned the handler without applying that proxy, so a navigation served
from the search-prefetch cache via navigation preload (or synthetic
response) was not visible to webRequest listeners as a tab-attributed
main_frame request.

This change makes MaybeProxyRequestHandler() static and applies it from
both entry points, plumbing navigation_id and
navigation_response_task_runner through
CreateURLLoaderHandlerForServiceWorkerInitiatedNavigationRequest to
match WillCreateURLLoaderRequestInterceptors().

TAG=agy
CONV=f241a2fc-8d6a-41d2-ad29-0b59cc158c4e

Bug: 513192482
Change-Id: I9e840912c235a12be087bbdf5617763053701c15
Fixed: 513192482
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8164421
Commit-Queue: Shunya Shishido <[email protected]>
Reviewed-by: Lingqi Chi <[email protected]>
Reviewed-by: Tsuyoshi Horo <[email protected]>
Reviewed-by: Yoshisato Yanagisawa <[email protected]>
Reviewed-by: Rakina Zata Amni <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1673099}
---

diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc
index 88b1cec..d416416 100644
--- a/chrome/browser/chrome_content_browser_client.cc
+++ b/chrome/browser/chrome_content_browser_client.cc
@@ -6769,7 +6769,10 @@
 ChromeContentBrowserClient::
     CreateURLLoaderHandlerForServiceWorkerInitiatedNavigationRequest(
         content::FrameTreeNodeId frame_tree_node_id,
-        const network::ResourceRequest& resource_request) {
+        const network::ResourceRequest& resource_request,
+        int64_t navigation_id,
+        scoped_refptr<base::SequencedTaskRunner>
+            navigation_response_task_runner) {
   // Note: SearchPrefetchService only applies to omnibox searches, which are not
   // in scope for Connection Allowlist intervention. However, if we ever intend
   // to create a loader in this function on behalf of a specific context, then
@@ -6777,6 +6780,13 @@
   SearchPrefetchURLLoader::RequestHandler prefetch_handler =
       SearchPrefetchURLLoaderInterceptor::MaybeCreateLoaderForRequest(
           resource_request, frame_tree_node_id);
+  if (prefetch_handler) {
+    prefetch_handler =
+        SearchPrefetchURLLoaderInterceptor::MaybeProxyRequestHandler(
+            frame_tree_node_id, navigation_id,
+            std::move(navigation_response_task_runner),
+            std::move(prefetch_handler));
+  }
   return prefetch_handler;
 }
 
diff --git a/chrome/browser/chrome_content_browser_client.h b/chrome/browser/chrome_content_browser_client.h
index b09f62f..11bc050 100644
--- a/chrome/browser/chrome_content_browser_client.h
+++ b/chrome/browser/chrome_content_browser_client.h
@@ -716,7 +716,10 @@
   content::ContentBrowserClient::URLLoaderRequestHandler
   CreateURLLoaderHandlerForServiceWorkerInitiatedNavigationRequest(
       content::FrameTreeNodeId frame_tree_node_id,
-      const network::ResourceRequest& resource_request) override;
+      const network::ResourceRequest& resource_request,
+      int64_t navigation_id,
+      scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner)
+      override;
   bool WillInterceptWebSocket(content::RenderFrameHost* frame) override;
   content::ContentBrowserClient::WebSocketOptions GetWebSocketOptions(
       content::RenderFrameHost* frame) override;
diff --git a/chrome/browser/preloading/prefetch/search_prefetch/search_prefetch_service_browsertest.cc b/chrome/browser/preloading/prefetch/search_prefetch/search_prefetch_service_browsertest.cc
index cb7627c..2b2da2f 100644
--- a/chrome/browser/preloading/prefetch/search_prefetch/search_prefetch_service_browsertest.cc
+++ b/chrome/browser/preloading/prefetch/search_prefetch/search_prefetch_service_browsertest.cc
@@ -72,6 +72,14 @@
 #include "content/public/test/preloading_test_util.h"
 #include "content/public/test/test_navigation_observer.h"
 #include "content/public/test/url_loader_interceptor.h"
+#include "extensions/buildflags/buildflags.h"
+#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
+#include "chrome/browser/extensions/chrome_test_extension_loader.h"
+#include "extensions/browser/background_script_executor.h"
+#include "extensions/common/extension.h"
+#include "extensions/test/extension_test_message_listener.h"
+#include "extensions/test/test_extension_dir.h"
+#endif
 #include "net/base/features.h"
 #include "net/base/network_interfaces.h"
 #include "net/base/url_util.h"
@@ -3115,6 +3123,112 @@
   closure.Run();
 }
 
+#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
+IN_PROC_BROWSER_TEST_F(SearchPrefetchServiceEnabledBrowserTest,
+                       ServiceWorkerServedPrefetchVisibleToWebRequest) {
+  const GURL worker_url = GetSearchServerQueryURLWithNoQuery(kServiceWorkerUrl);
+  const std::string kEnableNavigationPreloadScript = R"(
+      self.addEventListener('activate', event => {
+          event.waitUntil(self.registration.navigationPreload.enable());
+        });
+      self.addEventListener('fetch', event => {
+          if (event.preloadResponse !== undefined) {
+            event.respondWith(
+              (async function() {
+                const response = await event.preloadResponse;
+                if (response) return response;
+                return fetch(event.request);
+              })()
+            );
+          }
+        });
+      )";
+  std::string search_terms = "prefetch_content";
+
+  auto [prefetch_url, search_url] =
+      GetSearchPrefetchAndNonPrefetch(search_terms);
+  GURL canonical_search_url = GetCanonicalSearchURL(prefetch_url);
+
+  RegisterStaticFile(kServiceWorkerUrl, kEnableNavigationPreloadScript,
+                     "text/javascript");
+
+  extensions::TestExtensionDir test_extension_dir;
+  test_extension_dir.WriteManifest(
+      R"({
+           "name": "WebRequest Monitor",
+           "manifest_version": 3,
+           "version": "0.1",
+           "permissions": ["webRequest"],
+           "host_permissions": ["*://*/*"],
+           "background": { "service_worker": "background.js" }
+         })");
+  test_extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
+                               R"(
+        var observed_main_frame = false;
+        chrome.webRequest.onBeforeRequest.addListener(function(details) {
+          if (details.type === 'main_frame' && details.tabId >= 0 &&
+              details.url.indexOf('prefetch_content') !== -1) {
+            observed_main_frame = true;
+          }
+        }, {urls: ["*://*/*"]});
+        chrome.test.sendMessage('ready');
+      )");
+
+  ExtensionTestMessageListener ready_listener("ready");
+  extensions::ChromeTestExtensionLoader extension_loader(
+      browser()->GetProfile());
+  scoped_refptr<const extensions::Extension> extension =
+      extension_loader.LoadExtension(test_extension_dir.UnpackedPath());
+  ASSERT_TRUE(extension);
+  ASSERT_TRUE(ready_listener.WaitUntilSatisfied());
+
+  auto* service_worker_context = browser()
+                                     ->GetProfile()
+                                     ->GetDefaultStoragePartition()
+                                     ->GetServiceWorkerContext();
+
+  base::RunLoop run_loop;
+  blink::mojom::ServiceWorkerRegistrationOptions options(
+      GetSearchServerQueryURLWithNoQuery("/"),
+      blink::mojom::ScriptType::kClassic,
+      blink::mojom::ServiceWorkerUpdateViaCache::kImports);
+  const blink::StorageKey key =
+      blink::StorageKey::CreateFirstParty(url::Origin::Create(options.scope));
+  service_worker_context->RegisterServiceWorker(
+      worker_url, key, options, content::GlobalRenderFrameHostId(),
+      base::BindOnce(&RunFirstParam, run_loop.QuitClosure()));
+  run_loop.Run();
+
+  auto* search_prefetch_service =
+      SearchPrefetchServiceFactory::GetForProfile(browser()->GetProfile());
+  EXPECT_NE(nullptr, search_prefetch_service);
+
+  EXPECT_TRUE(search_prefetch_service->MaybePrefetchURL(prefetch_url,
+                                                        GetWebContents()));
+  WaitUntilStatusChangesTo(canonical_search_url,
+                           SearchPrefetchStatus::kComplete);
+
+  auto prefetch_status =
+      search_prefetch_service->GetSearchPrefetchStatusForTesting(
+          canonical_search_url);
+  ASSERT_TRUE(prefetch_status.has_value());
+  EXPECT_EQ(SearchPrefetchStatus::kComplete, prefetch_status.value());
+
+  ASSERT_TRUE(content::NavigateToURL(GetWebContents(), search_url));
+
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/preloading/prefetch/search_prefetch/search_prefetch_service_browsertest.cc b/chrome/browser/preloading/prefetch/search_prefetch/search_prefetch_service_browsertest.cc
index cb7627c..2b2da2f 100644
--- a/chrome/browser/preloading/prefetch/search_prefetch/search_prefetch_service_browsertest.cc
+++ b/chrome/browser/preloading/prefetch/search_prefetch/search_prefetch_service_browsertest.cc
@@ -72,6 +72,14 @@
 #include "content/public/test/preloading_test_util.h"
 #include "content/public/test/test_navigation_observer.h"
 #include "content/public/test/url_loader_interceptor.h"
+#include "extensions/buildflags/buildflags.h"
+#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
+#include "chrome/browser/extensions/chrome_test_extension_loader.h"
+#include "extensions/browser/background_script_executor.h"
+#include "extensions/common/extension.h"
+#include "extensions/test/extension_test_message_listener.h"
+#include "extensions/test/test_extension_dir.h"
+#endif
 #include "net/base/features.h"
 #include "net/base/network_interfaces.h"
 #include "net/base/url_util.h"
@@ -3115,6 +3123,112 @@
   closure.Run();
 }
 
+#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
+IN_PROC_BROWSER_TEST_F(SearchPrefetchServiceEnabledBrowserTest,
+                       ServiceWorkerServedPrefetchVisibleToWebRequest) {
+  const GURL worker_url = GetSearchServerQueryURLWithNoQuery(kServiceWorkerUrl);
+  const std::string kEnableNavigationPreloadScript = R"(
+      self.addEventListener('activate', event => {
+          event.waitUntil(self.registration.navigationPreload.enable());
+        });
+      self.addEventListener('fetch', event => {
+          if (event.preloadResponse !== undefined) {
+            event.respondWith(
+              (async function() {
+                const response = await event.preloadResponse;
+                if (response) return response;
+                return fetch(event.request);
+              })()
+            );
+          }
+        });
+      )";
+  std::string search_terms = "prefetch_content";
+
+  auto [prefetch_url, search_url] =
+      GetSearchPrefetchAndNonPrefetch(search_terms);
+  GURL canonical_search_url = GetCanonicalSearchURL(prefetch_url);
+
+  RegisterStaticFile(kServiceWorkerUrl, kEnableNavigationPreloadScript,
+                     "text/javascript");
+
+  extensions::TestExtensionDir test_extension_dir;
+  test_extension_dir.WriteManifest(
+      R"({
+           "name": "WebRequest Monitor",
+           "manifest_version": 3,
+           "version": "0.1",
+           "permissions": ["webRequest"],
+           "host_permissions": ["*://*/*"],
+           "background": { "service_worker": "background.js" }
+         })");
+  test_extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
+                               R"(
+        var observed_main_frame = false;
+        chrome.webRequest.onBeforeRequest.addListener(function(details) {
+          if (details.type === 'main_frame' && details.tabId >= 0 &&
+              details.url.indexOf('prefetch_content') !== -1) {
+            observed_main_frame = true;
+          }
+        }, {urls: ["*://*/*"]});
+        chrome.test.sendMessage('ready');
+      )");
+
+  ExtensionTestMessageListener ready_listener("ready");
+  extensions::ChromeTestExtensionLoader extension_loader(
+      browser()->GetProfile());
+  scoped_refptr<const extensions::Extension> extension =
+      extension_loader.LoadExtension(test_extension_dir.UnpackedPath());
+  ASSERT_TRUE(extension);
+  ASSERT_TRUE(ready_listener.WaitUntilSatisfied());
+
+  auto* service_worker_context = browser()
+                                     ->GetProfile()
+                                     ->GetDefaultStoragePartition()
+                                     ->GetServiceWorkerContext();
+
+  base::RunLoop run_loop;
+  blink::mojom::ServiceWorkerRegistrationOptions options(
+      GetSearchServerQueryURLWithNoQuery("/"),
+      blink::mojom::ScriptType::kClassic,
+      blink::mojom::ServiceWorkerUpdateViaCache::kImports);
+  const blink::StorageKey key =
+      blink::StorageKey::CreateFirstParty(url::Origin::Create(options.scope));
+  service_worker_context->RegisterServiceWorker(
+      worker_url, key, options, content::GlobalRenderFrameHostId(),
+      base::BindOnce(&RunFirstParam, run_loop.QuitClosure()));
+  run_loop.Run();
+
+  auto* search_prefetch_service =
+      SearchPrefetchServiceFactory::GetForProfile(browser()->GetProfile());
+  EXPECT_NE(nullptr, search_prefetch_service);
+
+  EXPECT_TRUE(search_prefetch_service->MaybePrefetchURL(prefetch_url,
+                                                        GetWebContents()));
+  WaitUntilStatusChangesTo(canonical_search_url,
+                           SearchPrefetchStatus::kComplete);
+
+  auto prefetch_status =
+      search_prefetch_service->GetSearchPrefetchStatusForTesting(
+          canonical_search_url);
+  ASSERT_TRUE(prefetch_status.has_value());
+  EXPECT_EQ(SearchPrefetchStatus::kComplete, prefetch_status.value());
+
+  ASSERT_TRUE(content::NavigateToURL(GetWebContents(), search_url));
+
+  auto inner_html = GetDocumentInnerHTML();
+  EXPECT_FALSE(inner_html.contains("regular"));
+  EXPECT_TRUE(inner_html.contains("prefetch"));
+
+  ExtensionTestMessageListener check_listener;
+  extensions::BackgroundScriptExecutor::ExecuteScriptAsync(
+      browser()->GetProfile(), extension->id(),
+      "chrome.test.sendMessage(observed_main_frame ? 'seen' : 'not_seen');");
+  EXPECT_TRUE(check_listener.WaitUntilSatisfied());
+  EXPECT_EQ("seen", check_listener.message());
+}
+#endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE)
+
 IN_PROC_BROWSER_TEST_F(SearchPrefetchServiceEnabledBrowserTest,
                        ServiceWorkerServedPrefetchWithPreload) {
   const GURL worker_url = GetSearchServerQueryURLWithNoQuery(kServiceWorkerUrl);
diff --git a/content/browser/service_worker/service_worker_browsertest.cc b/content/browser/service_worker/service_worker_browsertest.cc
index 91b79380..c7124d4 100644
--- a/content/browser/service_worker/service_worker_browsertest.cc
+++ b/content/browser/service_worker/service_worker_browsertest.cc
@@ -8386,7 +8386,10 @@
   URLLoaderRequestHandler
   CreateURLLoaderHandlerForServiceWorkerInitiatedNavigationRequest(
       FrameTreeNodeId frame_tree_node_id,
-      const network::ResourceRequest& resource_request) override {
+      const network::ResourceRequest& resource_request,
+      int64_t navigation_id,
+      scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner)
+      override {
     if (intercept_) {
       return base::BindOnce(
           &MockContentBrowserClientWithInterceptor::HandleRequest,
diff --git a/content/browser/service_worker/service_worker_main_resource_loader_unittest.cc b/content/browser/service_worker/service_worker_main_resource_loader_unittest.cc
index ee36f538..f259bc2 100644
--- a/content/browser/service_worker/service_worker_main_resource_loader_unittest.cc
+++ b/content/browser/service_worker/service_worker_main_resource_loader_unittest.cc
@@ -20,6 +20,8 @@
 #include "components/services/storage/public/mojom/cache_storage_control.mojom.h"
 #include "content/browser/loader/navigation_loader_interceptor.h"
 #include "content/browser/loader/response_head_update_params.h"
+#include "content/browser/renderer_host/frame_tree_node.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
 #include "content/browser/service_worker/embedded_worker_test_helper.h"
 #include "content/browser/service_worker/fake_embedded_worker_instance_client.h"
 #include "content/browser/service_worker/fake_service_worker.h"
@@ -38,7 +40,10 @@
 #include "content/public/common/content_features.h"
 #include "content/public/test/browser_task_environment.h"
 #include "content/public/test/mock_render_process_host.h"
+#include "content/public/test/navigation_simulator.h"
 #include "content/public/test/test_content_browser_client.h"
+#include "content/public/test/test_renderer_host.h"
+#include "content/public/test/web_contents_tester.h"
 #include "content/test/fake_network_url_loader_factory.h"
 #include "mojo/public/cpp/system/data_pipe_utils.h"
 #include "net/test/cert_test_util.h"
@@ -71,7 +76,10 @@
   URLLoaderRequestHandler
   CreateURLLoaderHandlerForServiceWorkerInitiatedNavigationRequest(
       FrameTreeNodeId frame_tree_node_id,
-      const network::ResourceRequest& resource_request) override {
+      const network::ResourceRequest& resource_request,
+      int64_t navigation_id,
+      scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner)
+      override {
     if (handler_) {
       return std::move(handler_);
     }
@@ -598,6 +606,17 @@
     return helper_->context()->GetStorageControl();
   }
 
+  void SetUpTestWebContentsAndNavigation(const GURL& url) {
+    web_contents_ = WebContentsTester::CreateTestWebContents(
+        helper_->browser_context(), nullptr);
+    frame_tree_node_id_ =
+        web_contents_->GetPrimaryMainFrame()->GetFrameTreeNodeId();
+
+    navigation_simulator_ =
+        NavigationSimulator::CreateBrowserInitiated(url, web_contents_.get());
+    navigation_simulator_->Start();
+  }
+
   // Starts a request. After calling this, the request is ongoing and the
   // caller can use functions like client_.RunUntilComplete() to wait for
   // completion.
@@ -605,8 +624,10 @@
     // Create a ServiceWorkerClient and simulate what
     // ServiceWorkerControlleeRequestHandler does to assign it a controller.
     if (!service_worker_client_) {
-      service_worker_client_ = std::make_unique<ScopedServiceWorkerClient>(
-          CreateServiceWorkerClient(helper_->context(), request->url));
+      service_worker_client_ =
+          std::make_unique<ScopedServiceWorkerClient>(CreateServiceWorkerClient(
+              helper_->context(), request->url,
+              /*are_ancestors_secure=*/true, frame_tree_node_id_));
       service_worker_client()->AddMatchingRegistration(registration_.get());
       service_worker_client()->SetControllerRegistration(
           registration_, /*notify_controllerchange=*/false);
@@ -939,6 +960,11 @@
   bool did_call_fallback_callback_ = false;
   base::OnceClosure quit_closure_for_fallback_callback_;
   ResponseHeadUpdateParams response_head_update_params_;
+
+  RenderViewHostTestEnabler rvh_test_enabler_;
+  std::unique_ptr<WebContents> web_contents_;
+  std::unique_ptr<NavigationSimulator> navigation_simulator_;
+  FrameTreeNodeId frame_tree_node_id_;
 };
 
 TEST_F(ServiceWorkerMainResourceLoaderTest, Basic) {
@@ -1551,6 +1577,7 @@
   registration_->EnableNavigationPreload(true);
 
   std::unique_ptr<network::ResourceRequest> request = CreateRequest();
+  SetUpTestWebContentsAndNavigation(request->url);
   request->destination = network::mojom::RequestDestination::kFencedframe;
 
   // Perform the request.
@@ -2192,6 +2219,7 @@
       }));
 
   std::unique_ptr<network::ResourceRequest> request = CreateRequest();
+  SetUpTestWebContentsAndNavigation(request->url);
   request->is_outermost_main_frame = true;
 
   StartRequest(std::move(request));
Loading diff…

Original Bug Report

reported by [email protected]

Potential Extension WebRequest/DNR bypass in Search Prefetch via Service Worker navigation paths

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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: Search prefetch cache hits served through Service Worker navigation preload or synthetic response paths bypass extension monitoring (WebRequest and DeclarativeNetRequest). This occurs because the Service Worker navigation stack fails to apply the necessary extension proxying to the cached responses, allowing navigations to circumvent security policies.

Affected files:

  • chrome/browser/chrome_content_browser_client.cc
  • content/browser/service_worker/service_worker_client.cc
  • chrome/browser/preloading/prefetch/search_prefetch/search_prefetch_url_loader_interceptor.cc
  • content/browser/service_worker/service_worker_fetch_dispatcher.cc
  • content/browser/service_worker/service_worker_main_resource_loader.cc

Estimated timestamp from git blame: 2023-06-07

Description

A potential logic flaw has been identified in Chrome’s Service Worker navigation stack where search results served from the Search Prefetch cache bypass Extension-based monitoring and security policies (e.g., WebRequest and DeclarativeNetRequest).

Normally, when a navigation hit occurs in the Search Prefetch cache, the SearchPrefetchURLLoaderInterceptor ensures the response is wrapped in a proxying handler that extensions can observe. However, when a navigation is intercepted by a Service Worker—specifically via the Navigation Preload or Synthetic Response optimization paths—this proxying step is omitted.

Potential Root Cause Analysis

The issue appears to stem from missing proxying logic in three specific locations:

  1. Embedder Hook: In chrome/browser/chrome_content_browser_client.cc, the function CreateURLLoaderHandlerForServiceWorkerInitiatedNavigationRequest (lines 6680-6687) retrieves a handler for search prefetch hits by calling SearchPrefetchURLLoaderInterceptor::MaybeCreateLoaderForRequest directly. Unlike the standard navigation interceptor path, it fails to invoke MaybeProxyRequestHandler, resulting in an unmonitored handler being returned to the Service Worker stack.

  2. Navigation Preload Path: In content/browser/service_worker/service_worker_client.cc, for the kNavigationPreload case (lines 1281-1296), if the embedder returns a handler (the unproxied Search Prefetch handler), the code immediately returns a SingleRequestURLLoaderFactory wrapping that handler. This skips the subsequent call to WillCreateURLLoaderFactory (line 1328), which is the standard bottleneck for installing WebRequest proxies.

  3. Synthetic Response Path: In content/browser/service_worker/service_worker_main_resource_loader.cc, the MaybeStartSyntheticNetworkRequest function (lines 1087-1105) takes the unproxied handler and runs it directly (handler.value().Run(...)). This bypasses any URLLoaderFactory creation or extension proxying for the navigation.

Potential Impact

This bypass allows a website (specifically the user’s Default Search Engine) to serve cached content that circumvents extension-based security and auditing controls. This includes Enterprise Data Loss Prevention (DLP), SafeSearch enforcement, or privacy-enhancing extensions that strip headers.

While the initial speculative prefetch request is monitored, it is typically observed with a tabId of -1. Many security policies are specifically scoped to main_frame navigations or specific tab IDs. These policies are bypassed because the actual navigation event, when served from the cache via the Service Worker path, is never observed by the extension.

Suggested Potential Reproduction Steps

  1. Configure a test Default Search Engine (DSE) origin.
  2. Register a Service Worker on that origin that enables navigation preload: self.registration.navigationPreload.enable().
  3. In the Service Worker’s fetch handler, use the preload response: event.respondWith(event.preloadResponse).
  4. Install an extension that uses webRequest to block the DSE URL for main_frame navigations, conditioned on a valid tabId (> -1).
  5. Trigger a search prefetch by typing a query in the omnibox. The extension will see a prefetch request (tabId: -1) and will likely allow it.
  6. Commit the navigation (press Enter).
  7. Observation: If the SRP is served via the Service Worker synthetic or preload path, the content may be committed without the extension receiving a navigation-time WebRequest event, effectively bypassing the block rule.

Suggested Fix

  1. In chrome/browser/chrome_content_browser_client.cc, update CreateURLLoaderHandlerForServiceWorkerInitiatedNavigationRequest to wrap the returned handler in MaybeProxyRequestHandler (which may need to be exposed or the proxying logic abstracted).
  2. In the Service Worker stack, ensure that even when an embedder-provided handler is used for navigation preload, the resulting loader or factory is still passed through the standard extension interception hooks (WillCreateURLLoaderFactory).

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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