CVE-2026-87556
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/chrome_content_browser_client.cc |
modified | |
ifchrome/browser/preloading/prefetch/search_prefetch/search_prefetch_service_browsertest.cc |
modified |
Files Changed
chrome/browser/chrome_content_browser_client.ccchrome/browser/chrome_content_browser_client.hchrome/browser/preloading/prefetch/search_prefetch/search_prefetch_service_browsertest.cc
Patch
From c807650ddca97469999921e6537bade99e7c9322 Mon Sep 17 00:00:00 2001
From: Shunya Shishido <sisidovski@chromium.org>
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 <sisidovski@chromium.org>
Reviewed-by: Lingqi Chi <lingqi@chromium.org>
Reviewed-by: Tsuyoshi Horo <horo@chromium.org>
Reviewed-by: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
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));
+
Regression Test / PoC
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));
Original Bug Report
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.cccontent/browser/service_worker/service_worker_client.ccchrome/browser/preloading/prefetch/search_prefetch/search_prefetch_url_loader_interceptor.cccontent/browser/service_worker/service_worker_fetch_dispatcher.cccontent/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:
-
Embedder Hook: In
chrome/browser/chrome_content_browser_client.cc, the functionCreateURLLoaderHandlerForServiceWorkerInitiatedNavigationRequest(lines 6680-6687) retrieves a handler for search prefetch hits by callingSearchPrefetchURLLoaderInterceptor::MaybeCreateLoaderForRequestdirectly. Unlike the standard navigation interceptor path, it fails to invokeMaybeProxyRequestHandler, resulting in an unmonitored handler being returned to the Service Worker stack. -
Navigation Preload Path: In
content/browser/service_worker/service_worker_client.cc, for thekNavigationPreloadcase (lines 1281-1296), if the embedder returns a handler (the unproxied Search Prefetch handler), the code immediately returns aSingleRequestURLLoaderFactorywrapping that handler. This skips the subsequent call toWillCreateURLLoaderFactory(line 1328), which is the standard bottleneck for installing WebRequest proxies. -
Synthetic Response Path: In
content/browser/service_worker/service_worker_main_resource_loader.cc, theMaybeStartSyntheticNetworkRequestfunction (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
- Configure a test Default Search Engine (DSE) origin.
- Register a Service Worker on that origin that enables navigation preload:
self.registration.navigationPreload.enable(). - In the Service Worker’s fetch handler, use the preload response:
event.respondWith(event.preloadResponse). - Install an extension that uses
webRequestto block the DSE URL formain_framenavigations, conditioned on a validtabId(> -1). - 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. - Commit the navigation (press Enter).
- 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
- In
chrome/browser/chrome_content_browser_client.cc, updateCreateURLLoaderHandlerForServiceWorkerInitiatedNavigationRequestto wrap the returned handler inMaybeProxyRequestHandler(which may need to be exposed or the proxying logic abstracted). - 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.