CVE-2026-7909
Overview
Background
- `ServiceWorkerForwardedRaceNetworkRequestURLLoaderFactory`
- a browser-side
URLLoaderFactoryused by the RaceNetworkRequest ServiceWorker feature to forward a single already-started network response’s data pipe to the renderer. - RaceNetworkRequest
- a ServiceWorker optimization that starts a direct network fetch in parallel with the service worker
fetchhandler and delivers whichever response is ready first. - `is_data_pipe_fused_`
- a boolean flag set once the factory has spliced (“fused”) its forwarded response data pipe into the client, marking the factory as already consumed.
- `ReportBadMessage`
- a Mojo receiver mechanism that marks an incoming IPC as malformed and terminates the calling (renderer) process.
Root Cause Analysis
The factory is designed for exactly one CreateLoaderAndStart call: the first call fuses the forwarded data pipe and sets is_data_pipe_fused_ = true. In the pre-patch code, a second CreateLoaderAndStart (the else branch, reached when is_data_pipe_fused_ is already true) silently fell back to fallback_factory_->CreateLoaderAndStart(...), quietly starting a brand-new request instead of treating the reuse as an error. Because a legitimate renderer never issues a second call on this single-use factory, that fallback path could only be reached by a compromised or buggy renderer, and honoring it let the renderer drive unexpected loader state through a factory that had already been consumed.
The fix replaces the fallback with receiver_.ReportBadMessage(...) (guarded by the default-enabled kill-switch feature kKillSwitchForRaceNetworkRequestMultipleCreateLoaderAndStartCalls), so a duplicate call now terminates the renderer rather than being serviced. This restores the single-use invariant by making illegal reuse fatal instead of silently tolerated.
CreateLoaderAndStart on a one-shot factory as a recoverable condition (fall back and start a new request) rather than as an IPC-contract violation; the fix converts that impossible-for-a-good-renderer state into a bad-message kill.Attack Path
- Establish a RaceNetworkRequest loader
A page controlled by a service worker triggers a navigation/subresource fetch that instantiates a
ServiceWorkerForwardedRaceNetworkRequestURLLoaderFactoryin the browser process. - Consume the factory once
The first
CreateLoaderAndStartfuses the forwarded data pipe and setsis_data_pipe_fused_ = true, leaving the factory in its post-use state. - Reuse the factory
A compromised renderer sends a second
CreateLoaderAndStarton the same factory receiver, reaching theelsebranch that a legitimate renderer never hits. - Trigger unexpected browser-side loader behavior
Pre-patch, the browser served this by calling
fallback_factory_->CreateLoaderAndStart(...), starting an unintended request through an already-consumed factory (the concrete downstream corruption or confusion this enables is not shown in the diff)(inferred).
Impact Assessment
CreateLoaderAndStart IPC. No exploited-in-the-wild status is stated, and any escalation beyond the mishandled reuse is (inferred).Files Changed
content/common/service_worker/forwarded_race_network_request_url_loader_factory.cc
Audit Directions
- Single-use IPC objects with silent fallbackAudit other Mojo-exposed factories/loaders that maintain a “consumed”/“fused” flag but tolerate re-entry by re-doing work instead of reporting a bad message.
- `else`-branch permissiveness on renderer-driven stateLook for browser-side handlers that treat a should-be-impossible renderer state as recoverable (fallback, retry, no-op) rather than as a protocol violation.
- RaceNetworkRequest / forwarding loadersReview the broader RaceNetworkRequest and ServiceWorker loader plumbing for other places where forwarded pipes or receivers can be re-driven after their one intended use.
Patch
From f96ee4aeefc76c420782298b64f41e6dfa1f1db2 Mon Sep 17 00:00:00 2001
From: Shunya Shishido <sisidovski@chromium.org>
Date: Thu, 02 Apr 2026 08:23:44 -0700
Subject: [PATCH] Report bad message on reuse of ForwardedRaceNetworkRequestURLLoaderFactory
The ForwardedRaceNetworkRequestURLLoaderFactory is intended for a single
use. If CreateLoaderAndStart is called after the data pipe has already
been fused, it indicates an unexpected state, so report a bad message
instead of falling back.
Bug: 497437113
Change-Id: I2867a9359a6764065fe95e0b1f7d68fc80cf0d05
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7723101
Commit-Queue: Shunya Shishido <sisidovski@chromium.org>
Reviewed-by: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1609228}
---
diff --git a/content/common/service_worker/forwarded_race_network_request_url_loader_factory.cc b/content/common/service_worker/forwarded_race_network_request_url_loader_factory.cc
index a64f9b5..cfd03bec 100644
--- a/content/common/service_worker/forwarded_race_network_request_url_loader_factory.cc
+++ b/content/common/service_worker/forwarded_race_network_request_url_loader_factory.cc
@@ -4,8 +4,16 @@
#include "content/common/service_worker/forwarded_race_network_request_url_loader_factory.h"
+#include "base/feature_list.h"
+
namespace content {
+namespace {
+// Kill switch for multiple CreateLoaderAndStart calls.
+BASE_FEATURE(kKillSwitchForRaceNetworkRequestMultipleCreateLoaderAndStartCalls,
+ base::FEATURE_ENABLED_BY_DEFAULT);
+} // namespace
+
ServiceWorkerForwardedRaceNetworkRequestURLLoaderFactory::
ServiceWorkerForwardedRaceNetworkRequestURLLoaderFactory(
mojo::PendingReceiver<network::mojom::URLLoaderClient> client_receiver,
@@ -34,10 +42,14 @@
CHECK(result) << resource_request.url;
is_data_pipe_fused_ = true;
} else {
- // If already fused, create a new URLLoader and start the new request.
- fallback_factory_->CreateLoaderAndStart(
- std::move(receiver), request_id, options, resource_request,
- std::move(client), traffic_annotation);
+ // A legitimate renderer will never hit this branch.
+ // If we are here, the renderer is compromised or severely buggy.
+ if (base::FeatureList::IsEnabled(
+ kKillSwitchForRaceNetworkRequestMultipleCreateLoaderAndStartCalls)) {
+ receiver_.ReportBadMessage(
+ "ServiceWorkerForwardedRaceNetworkRequestURLLoaderFactory: "
+ "CreateLoaderAndStart called multiple times.");
+ }
}
}
Original Bug Report
Site Isolation bypass via ServiceWorkerForwardedRaceNetworkRequestURLLoaderFactory
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A potential vulnerability in ServiceWorkerForwardedRaceNetworkRequestURLLoaderFactory allows a compromised Service Worker renderer to bypass Site Isolation. By calling CreateLoaderAndStart twice on a provided Mojo pipe, the renderer can forward arbitrary requests to a trusted browser-process URLLoaderFactory, enabling cross-origin data theft.
Affected files:
content/common/service_worker/forwarded_race_network_request_url_loader_factory.cccontent/browser/service_worker/service_worker_main_resource_loader.cccontent/browser/service_worker/service_worker_fetch_dispatcher.cccontent/browser/service_worker/service_worker_client.cccontent/browser/loader/navigation_url_loader_impl.ccservices/network/cors/cors_url_loader_factory.cc
Estimated timestamp from git blame: 2025-12-17
Summary
A potential vulnerability exists in the Service Worker ‘Race Network Request’ implementation where a highly trusted URLLoaderFactory is exposed to the renderer process with insufficient validation. A compromised Service Worker renderer can abuse this factory’s state machine to make arbitrary network requests with browser-process privileges, effectively bypassing Site Isolation, Opaque Response Blocking (ORB), and the request_initiator origin lock.
Vulnerability Details
When a Race Network Request is initiated for a main resource, ServiceWorkerMainResourceLoader::StartRaceNetworkRequest (in content/browser/service_worker/service_worker_main_resource_loader.cc) creates a URLLoaderFactory intended for the browser process. This factory is created via NavigationURLLoaderImpl::CreateURLLoaderFactoryWithHeaderClient and is granted elevated privileges:
process_id=network::mojom::kBrowserProcessIdis_trusted=trueis_orb_enabled=false
This privileged factory is passed as the fallback_factory_ to a new instance of ServiceWorkerForwardedRaceNetworkRequestURLLoaderFactory. A Mojo PendingRemote to this wrapper factory is then sent to the untrusted Service Worker renderer via DispatchFetchEventParams.race_network_request_loader_factory.
In content/common/service_worker/forwarded_race_network_request_url_loader_factory.cc, the CreateLoaderAndStart method has a logic flaw:
void ServiceWorkerForwardedRaceNetworkRequestURLLoaderFactory::CreateLoaderAndStart(
/* ... */
const network::ResourceRequest& resource_request,
/* ... */) {
if (!is_data_pipe_fused_) {
bool result = mojo::FusePipes(std::move(client_receiver_), std::move(client));
CHECK(result) << resource_request.url;
result = mojo::FusePipes(std::move(receiver), std::move(loader_));
CHECK(result) << resource_request.url;
is_data_pipe_fused_ = true;
} else {
fallback_factory_->CreateLoaderAndStart(
std::move(receiver), request_id, options, resource_request,
std::move(client), traffic_annotation);
}
}
A compromised renderer can call this method twice:
- First Call: The renderer calls
CreateLoaderAndStartwith valid, unused Mojo endpoints.mojo::FusePipessucceeds, andis_data_pipe_fused_is set totrue. - Second Call: The renderer calls
CreateLoaderAndStartagain with a maliciously craftedResourceRequest(e.g., targeting a sensitive cross-origin URL, spoofingrequest_initiator, and injectingtrusted_params). Becauseis_data_pipe_fused_is nowtrue, the code enters theelsebranch and blindly forwards the attacker’s request to the highly privilegedfallback_factory_.
Because the fallback_factory_ is trusted by the Network Service, it skips standard renderer security checks. It accepts the spoofed request_initiator without killing the renderer, processes the injected trusted_params (attaching the victim’s cross-origin cookies), and disables ORB. The renderer can then read the raw cross-origin response, resulting in a full Site Isolation bypass.
Potential Reproduction Steps
Note: These are suggested steps based on code analysis; we do not have a working Proof of Concept yet.
- An attacker registers a Service Worker using the Static Router API with a
race-network-and-fetch-handlersource (or relies onServiceWorkerAutoPreload). - The attacker compromises the renderer process hosting the Service Worker (e.g., via a V8 vulnerability).
- A navigation occurs that triggers the race request, causing the browser process to send a
DispatchFetchEventIPC containing therace_network_request_loader_factoryMojo remote. - From the compromised renderer, the attacker intercepts this remote.
- To keep the factory alive in the browser process, the attacker’s script intentionally delays or never finishes the
fetchevent. - The attacker creates valid, unused Mojo pipes and calls
CreateLoaderAndStarton the remote. This flips the internalis_data_pipe_fused_state totruein the browser process. - The attacker crafts a malicious
ResourceRequesttargeting a cross-origin URL (e.g.,https://mail.google.com), sets therequest_initiatorto match the target, and injectstrusted_paramswith appropriateisolation_info. - The attacker calls
CreateLoaderAndStartagain on the same remote with this payload. The browser process forwards it to the Network Service using its trusted factory. - The Network Service fulfills the request, attaches the victim’s cookies, and streams the cross-origin response back to the compromised renderer, completing the data theft.
Suggested Fix
The ServiceWorkerForwardedRaceNetworkRequestURLLoaderFactory should strictly enforce that it only handles a single request, or it must validate the incoming ResourceRequest.
A potential fix is to disconnect the receiver and destroy the factory immediately after the first CreateLoaderAndStart call if the intention is only to fuse pipes. If the fallback_factory_ must be used for subsequent calls (e.g., for redirects), the wrapper must validate that the incoming ResourceRequest perfectly matches the original request parameters (URL, initiator, etc.) and strictly reject or strip trusted_params coming from the untrusted renderer.
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.