CVE-2026-17662
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/preloading/prefetch/prefetch_url_loader_interceptor.cc |
modified | |
PrefetchContainercontent/browser/preloading/prefetch/prefetch_url_loader_interceptor.h |
modified | |
PrefetchServicecontent/browser/preloading/prefetch/prefetch_url_loader_interceptor.h |
modified | |
ServiceWorkerMainResourceHandlecontent/browser/preloading/prefetch/prefetch_url_loader_interceptor.h |
modified |
Files Changed
content/browser/preloading/prefetch/prefetch_url_loader_interceptor.cccontent/browser/preloading/prefetch/prefetch_url_loader_interceptor.h
Patch
From 7ac3a935b453ceb4835c41eb0c65202b07be68f0 Mon Sep 17 00:00:00 2001 From: Hiroshige Hayashizaki <[email protected]> Date: Tue, 04 Aug 2026 22:03:19 -0700 Subject: [PATCH] Move PrefetchService check a bit Praparation for https://crrev.com/c/7952998. This shouldn't impact the behavior much, as the `PrefetchService` check failure is rare, and it's probably correct to always reject serving when `PrefetchService` is gone. Bug: 532959369, 497491557 Change-Id: I23288ff4401feccfeefa9a1af8f61a0ba03e3820 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7950630 Reviewed-by: Taiyo Mizuhashi <[email protected]> Commit-Queue: Hiroshige Hayashizaki <[email protected]> Cr-Commit-Position: refs/heads/main@{#1673897} --- diff --git a/content/browser/preloading/prefetch/prefetch_url_loader_interceptor.cc b/content/browser/preloading/prefetch/prefetch_url_loader_interceptor.cc index e2323cd..7e03c954 100644 --- a/content/browser/preloading/prefetch/prefetch_url_loader_interceptor.cc +++ b/content/browser/preloading/prefetch/prefetch_url_loader_interceptor.cc @@ -130,6 +130,15 @@ return; } + PrefetchService* prefetch_service = + PrefetchService::GetFromFrameTreeNodeId(frame_tree_node_id_); + if (!prefetch_service) { + redirect_serving_handle_ = PrefetchServingHandle(); + TRACE_EVENT_END("loading"); + std::move(loader_callback_).Run(std::nullopt); + return; + } + if (redirect_serving_handle_ && redirect_serving_handle_.DoesCurrentURLToServeMatch( tentative_resource_request.url)) { @@ -184,7 +193,7 @@ TRACE_EVENT_END("loading"); GetPrefetch( - tentative_resource_request.url, + *prefetch_service, tentative_resource_request.url, base::BindOnce(&PrefetchURLLoaderInterceptor::OnGetPrefetchComplete, weak_factory_.GetWeakPtr(), tentative_resource_request.url, @@ -194,6 +203,7 @@ } void PrefetchURLLoaderInterceptor::GetPrefetch( + PrefetchService& prefetch_service, const GURL& url, base::OnceCallback<void(PrefetchServingHandle)> get_prefetch_callback) const { @@ -201,21 +211,12 @@ perfetto::Flow::FromPointer( const_cast<PrefetchURLLoaderInterceptor*>(this))); - PrefetchService* prefetch_service = - PrefetchService::GetFromFrameTreeNodeId(frame_tree_node_id_); - if (!prefetch_service) { - TRACE_EVENT_END("loading"); - std::move(get_prefetch_callback).Run({}); - return; - } - - auto callback = base::BindOnce(&OnGotPrefetchToServe, frame_tree_node_id_, url, std::move(get_prefetch_callback)); auto key = PrefetchKey(initiator_document_token_, url); TRACE_EVENT_END("loading"); PrefetchMatchResolver::FindPrefetch( - frame_tree_node_id_, *prefetch_service, std::move(key), + frame_tree_node_id_, prefetch_service, std::move(key), expected_service_worker_state_, std::move(callback), perfetto::Flow::FromPointer( const_cast<PrefetchURLLoaderInterceptor*>(this))); diff --git a/content/browser/preloading/prefetch/prefetch_url_loader_interceptor.h b/content/browser/preloading/prefetch/prefetch_url_loader_interceptor.h index 54e3457b..4d1409e 100644 --- a/content/browser/preloading/prefetch/prefetch_url_loader_interceptor.h +++ b/content/browser/preloading/prefetch/prefetch_url_loader_interceptor.h @@ -18,6 +18,7 @@ namespace content { class PrefetchContainer; +class PrefetchService; class ServiceWorkerMainResourceHandle; using PrefetchCompleteCallbackForTesting = @@ -60,7 +61,8 @@ // from `PrefetchService` and then goes through other checks in // `PrefetchUrlLoaderHelper`. // The |get_prefetch_callback| is called with this associated prefetch. - void GetPrefetch(const GURL& url, + void GetPrefetch(PrefetchService& prefetch_service, + const GURL& url, base::OnceCallback<void(PrefetchServingHandle)> get_prefetch_callback) const;
Original Bug Report
StoragePartition isolation bypass via Speculation Rules Prefetch ServiceWorker inheritance
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A logic flaw in Chrome’s prefetching system allows a navigation in a non-default StoragePartition (e.g., a guest <webview>) to inherit a ServiceWorker controller from the default StoragePartition. This occurs because the prefetch path hardcodes the default StoragePartition for ServiceWorker checks and network fetching, and the subsequent matching logic fails to verify the StoragePartition identity before inheriting the controller. This allows an attacker in an isolated context to read cross-origin data intended for the user’s primary profile.
Affected files:
content/browser/service_worker/service_worker_main_resource_handle.cccontent/browser/preloading/prefetch/prefetch_streaming_url_loader.cccontent/browser/preloading/prefetch/prefetch_service.cccontent/browser/preloading/prefetch/prefetch_url_loader_interceptor.cccontent/browser/service_worker/service_worker_client.cccontent/browser/preloading/prefetch/prefetch_key.h
Estimated timestamp from git blame: 2026-02-13
Description
There is a potential StoragePartition isolation bypass in Chrome’s prefetching logic when handling Service Workers. PrefetchService manages prefetches at the BrowserContext level. However, when evaluating if a ServiceWorker controls a prefetch URL, it explicitly fetches the ServiceWorkerContext and NetworkContext from the default StoragePartition, regardless of the initiator’s partition (e.g., a guest <webview> or Controlled Frame).
See content/browser/preloading/prefetch/prefetch_service.cc lines 850-853:
ServiceWorkerContextWrapper* service_worker_context =
g_service_worker_context_for_testing
? g_service_worker_context_for_testing
: browser_context_->GetDefaultStoragePartition()
->GetServiceWorkerContext();
Later, when the guest context navigates to the prefetched URL, PrefetchURLLoaderInterceptor attempts to find a matching prefetch. The matching logic relies on PrefetchKey, which only encapsulates the target URL and the initiator’s DocumentToken, completely lacking any field representing the StoragePartition.
Upon a successful match, ServiceWorkerMainResourceHandle::InitializeForRequest is called to wire the guest navigation to the prefetch’s ServiceWorker. This method validates the prefetch client by comparing its top_frame_origin and blink::StorageKey against the navigation context. However, neither url::Origin nor blink::StorageKey encodes StoragePartition identity. Consequently, the check passes, and the navigation incorrectly inherits the default partition’s ServiceWorkerRegistration.
Potential Steps to Reproduce
Note: These are suggested steps; our tooling agent cannot execute code to provide a working PoC.
- A user visits
https://victim.testin a standard browser tab, registering a Service Worker in the defaultStoragePartition. - The user runs a malicious Chrome App or Isolated Web App containing a guest context (e.g.,
<webview>), which operates in a separate, isolatedStoragePartition. - From within the attacker-controlled guest context, a Speculation Rules prefetch (
<script type="speculationrules">) is triggered forhttps://victim.test. - The prefetch executes using the default
StoragePartition’s network context and ServiceWorker context. - The attacker then navigates the guest context to
https://victim.test. - The prefetch interceptor incorrectly matches the prefetch and adopts the default-partition ServiceWorker as the controller for the guest navigation.
- Subsequent subresource fetches from the guest document at
https://victim.testare routed through the default partition’s ServiceWorker, allowing the attacker to read authenticated or cached cross-origin data.
Impact
This vulnerability allows an attacker to completely bypass StoragePartition isolation boundaries. By controlling a guest context, the attacker can force it to be controlled by a Service Worker from the user’s primary, default profile. This enables the extraction of sensitive, authenticated data from arbitrary origins that the user has visited, leading to a severe cross-partition data leak.
Suggested Fix
- Enforce Partition Matching:
PrefetchKeyshould be updated to include theStoragePartition(or an identifier for it) so that prefetches initiated in an isolated partition cannot match navigations in the default partition, and vice versa. - Use Correct Contexts:
PrefetchServiceandPrefetchStreamingURLLoadermust use theServiceWorkerContextandNetworkContextassociated with the initiator’sStoragePartition, rather than unconditionally callingBrowserContext::GetDefaultStoragePartition(). - Strict Inheritance Checks:
ServiceWorkerMainResourceHandle::InitializeForRequestshould explicitly verify that theStoragePartitionof theclient_for_prefetchmatches theStoragePartitionof the ongoing navigation before callingSetControllerRegistration.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from 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. And please feel free to reach out to me directly if you have concerns or feedback on the project.