CVE-2026-19153
Overview
Background
- `WorkerScriptLoader`
- the browser-process (UI thread)
URLLoader/URLLoaderClientshim incontent/browser/worker_host/that fetches the main script of dedicated and shared workers and passes results to an outer client such asWorkerScriptFetcher. - `OnReceiveRedirect`
- the loader-client callback that receives a
net::RedirectInfo(holdingnew_url) and decides whether the redirect is forwarded and followed or the load is completed with an error. - `IsSafeRedirectTarget()`
- a content helper (from
content/public/common/url_utils.h) that decides whether a redirect from one URL to another is allowed; per the commit message it rejects targets such asfile://orchrome://. - `blob:` URL
- an in-memory object URL served by the blob system; per the patch comment, “Loading a blob URL never produces a redirect”.
Root Cause Analysis
WorkerScriptLoader::OnReceiveRedirect (named in the commit message; the hunk shows only its parameters, redirect_info and response_head) used to check only the redirect count (--redirect_limit_ == 0 → ERR_TOO_MANY_REDIRECTS) before passing a redirect to the outer client and following it. It never checked what was redirecting or where the redirect led. Two invariants were therefore not enforced for worker main-script loads, even though NavigationURLLoaderImpl already enforces them for main-frame navigations: a blob: load must never redirect, and a redirect must go to a scheme that IsSafeRedirectTarget() allows.
The patch adds two early-exit checks at the top of the function, before the redirect-limit check and before any forwarding. If resource_request_.url.SchemeIsBlob(), or if !IsSafeRedirectTarget(resource_request_.url, redirect_info.new_url), the loader sets complete_status_ to net::ERR_UNSAFE_REDIRECT, calls CommitCompleted() and returns. The new tests confirm that the outer client then sees no redirect (has_received_redirect() is false) and only a completion with ERR_UNSAFE_REDIRECT. The diff does not show which upstream loader can actually produce a redirect for a blob: worker script; the test injects one by hand.
blob: never redirects) and the target (IsSafeRedirectTarget()) before the redirect is forwarded or followed.Attack Path
- Page starts a worker
Web content creates a dedicated or shared worker, so the browser starts a
WorkerScriptLoaderfor the main script URL (either anhttps:URL or ablob:URL). - Loader receives a redirect
The underlying load returns a redirect: either an attacker server’s HTTP redirect to a scheme such as
file:orchrome:(as inRejectUnsafeRedirectTarget), or a redirect on ablob:load that should never happen (as inRedirectFromBlobUrl). How ablob:load would produce one in practice is not shown in the diff. - Redirect forwarded without validation
Before the patch,
OnReceiveRedirectonly decrementedredirect_limit_and then passed the redirect on to the outerURLLoaderClient(for exampleWorkerScriptFetcher) to be followed. - Worker script from an unintended origin or scheme
Following the redirect could let the worker’s script, and possibly the URL or origin the browser associates with it, come from a privileged or local scheme, or let a
blob:worker escape blob-origin semantics (inferred; the downstream effect is not shown in the patch).
Impact Assessment
blob: load, rather than memory corruption. Any resulting access to local files or privileged pages is (inferred), since IsSafeRedirectTarget() and other layers may already block some targets. Web content in a stock build can start workers and control the redirect responses of its own servers, but whether a blob: load can redirect without a compromised renderer or other special conditions is not established by the diff. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.1.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/worker_host/worker_script_loader.cc |
modified | |
TEST_Fcontent/browser/worker_host/worker_script_loader_factory_unittest.cc |
modified |
Files Changed
content/browser/worker_host/worker_script_loader.cccontent/browser/worker_host/worker_script_loader_factory_unittest.cc
Audit Directions
- Loader shims that only count redirectsAudit other browser-side
URLLoaderClientwrappers (service worker script loading,ServiceWorkerMainResourceLoaderInterceptorpaths, prefetch/prerender loaders, extension and WebUI loaders) that checkredirect_limit_but notIsSafeRedirectTarget()or the source scheme. - Schemes that must never redirectAny loader for
blob:,data:,filesystem:orabout:content should treat a redirect as a hard error. Look for paths where a renderer-supplied or interceptor-suppliedURLLoaderFactorycould produce one. - Parity with navigationCompare worker, shared-worker and service-worker script fetch policy with
NavigationURLLoaderImplfor redirects,URLLoaderThrottlechecks and scheme restrictions; the commit explicitly fixes such a parity gap.
Patch
From 22500d480098ce3bb02f8850a7e4f93bed85d335 Mon Sep 17 00:00:00 2001
From: Yoshisto Yanagisawa <yyanagisawa@chromium.org>
Date: Tue, 21 Jul 2026 21:58:39 -0700
Subject: [PATCH] Reject unsupported redirects during worker script loading
Loading a blob: URL is not expected to produce a redirect. To maintain
behavioral consistency with main frame navigations in
NavigationURLLoaderImpl and to follow expected resource loading
semantics, worker main script loading paths should decline unexpected or
unsupported redirect sequences.
This change updates WorkerScriptLoader::OnReceiveRedirect to immediately
complete script loads with net::ERR_UNSAFE_REDIRECT when:
- The originating request URL uses the blob: scheme.
- The destination URL scheme is not supported as a redirect target
according to IsSafeRedirectTarget() (e.g., attempting a redirect to
file:// or chrome://).
Handling this verification at WorkerScriptLoader guarantees that
unsupported redirects are cleanly terminated at the underlying URLLoader
boundary before ever reaching the outer URLLoaderClient (such as
WorkerScriptFetcher). This provides consistent error handling across
both renderer-initiated and browser-initiated worker loading flows.
Two unit test cases (RedirectFromBlobUrl and RejectUnsafeRedirectTarget)
are added to WorkerScriptLoaderFactoryTest to verify this error
handling.
TAG=agy
CONV=e9ed9a69-d0d2-4765-a7e4-262255687012
Bug: 532939327
Change-Id: I486a8e999b926472c7835234efb4f05d338d5ae6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8130577
Reviewed-by: Hiroki Nakagawa <nhiroki@chromium.org>
Auto-Submit: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Commit-Queue: Hiroki Nakagawa <nhiroki@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1666012}
---
diff --git a/content/browser/worker_host/worker_script_loader.cc b/content/browser/worker_host/worker_script_loader.cc
index 735a44a..dcccdae 100644
--- a/content/browser/worker_host/worker_script_loader.cc
+++ b/content/browser/worker_host/worker_script_loader.cc
@@ -12,7 +12,9 @@
#include "content/browser/service_worker/service_worker_main_resource_loader_interceptor.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
+#include "content/public/common/url_utils.h"
#include "net/base/load_timing_info.h"
+#include "net/base/net_errors.h"
#include "net/url_request/redirect_util.h"
#include "services/network/public/cpp/record_ontransfersizeupdate_utils.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
@@ -229,6 +231,22 @@
const net::RedirectInfo& redirect_info,
network::mojom::URLResponseHeadPtr response_head) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
+
+ if (resource_request_.url.SchemeIsBlob()) {
+ // Loading a blob URL never produces a redirect.
+ complete_status_ =
+ network::URLLoaderCompletionStatus(net::ERR_UNSAFE_REDIRECT);
+ CommitCompleted();
+ return;
+ }
+
+ if (!IsSafeRedirectTarget(resource_request_.url, redirect_info.new_url)) {
+ complete_status_ =
+ network::URLLoaderCompletionStatus(net::ERR_UNSAFE_REDIRECT);
+ CommitCompleted();
+ return;
+ }
+
if (--redirect_limit_ == 0) {
complete_status_ =
network::URLLoaderCompletionStatus(net::ERR_TOO_MANY_REDIRECTS);
diff --git a/content/browser/worker_host/worker_script_loader_factory_unittest.cc b/content/browser/worker_host/worker_script_loader_factory_unittest.cc
index d9d896b..4746b25f 100644
--- a/content/browser/worker_host/worker_script_loader_factory_unittest.cc
+++ b/content/browser/worker_host/worker_script_loader_factory_unittest.cc
@@ -6,6 +6,7 @@
#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
+#include "base/test/run_until.h"
#include "content/browser/service_worker/embedded_worker_test_helper.h"
#include "content/browser/service_worker/service_worker_client.h"
#include "content/browser/service_worker/service_worker_context_core.h"
@@ -16,6 +17,7 @@
#include "content/test/fake_network_url_loader_factory.h"
#include "net/base/isolation_info.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "net/url_request/redirect_info.h"
#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/test/test_url_loader_client.h"
@@ -186,6 +188,80 @@
EXPECT_EQ(net::ERR_ABORTED, client.completion_status().error_code);
}
+// Tests that a redirect received while loading a blob: URL is rejected. Loading
+// a blob URL never produces a redirect, so any redirect must be rejected before
+// it is forwarded or followed.
+TEST_F(WorkerScriptLoaderFactoryTest, RedirectFromBlobUrl) {
+ GURL url("blob:https://www.example.com/49146318-7a89-4041-9bcc-36e6b6eeef86");
+
+ // Defer the mock network load so we can inject a redirect on the in-flight
+ // load.
+ network_loader_factory_instance_->DeferHandleRequest();
+
+ // Create the factory.
+ auto factory = std::make_unique<WorkerScriptLoaderFactory>(
+ kProcessId, DedicatedOrSharedWorkerToken(),
+ net::IsolationInfo::CreateForInternalRequest(url::Origin::Create(url)),
+ service_worker_handle_.get(), browser_context_getter_,
+ network_loader_factory_);
+
+ // Start loading the script.
+ network::TestURLLoaderClient client;
+ mojo::PendingRemote<network::mojom::URLLoader> loader =
+ CreateTestLoaderAndStart(url, factory.get(), &client);
+ ASSERT_TRUE(base::test::RunUntil(
+ [&]() { return factory->GetScriptLoader() != nullptr; }));
+
+ // Simulate receiving a redirect from the blob load.
+ net::RedirectInfo redirect_info;
+ redirect_info.status_code = 302;
+ redirect_info.new_method = "GET";
+ redirect_info.new_url = GURL("https://other.example.com/worker.js");
+ factory->GetScriptLoader()->OnReceiveRedirect(
+ redirect_info, network::mojom::URLResponseHead::New());
+ client.RunUntilComplete();
+
+ // Verify that the redirect was blocked and the load was aborted with
+ // ERR_UNSAFE_REDIRECT.
+ EXPECT_FALSE(client.has_received_redirect());
+ ASSERT_TRUE(client.has_received_completion());
+ EXPECT_EQ(net::ERR_UNSAFE_REDIRECT, client.completion_status().error_code);
+}
+
+// Tests that a redirect to an unsafe target scheme is rejected.
+TEST_F(WorkerScriptLoaderFactoryTest, RejectUnsafeRedirectTarget) {
+ GURL url("https://www.example.com/worker.js");
+
+ network_loader_factory_instance_->DeferHandleRequest();
+
+ auto factory = std::make_unique<WorkerScriptLoaderFactory>(
+ kProcessId, DedicatedOrSharedWorkerToken(),
+ net::IsolationInfo::CreateForInternalRequest(url::Origin::Create(url)),
+ service_worker_handle_.get(), browser_context_getter_,
+ network_loader_factory_);
+
+ network::TestURLLoaderClient client;
+ mojo::PendingRemote<network::mojom::URLLoader> loader =
+ CreateTestLoaderAndStart(url, factory.get(), &client);
+ ASSERT_TRUE(base::test::RunUntil(
+ [&]() { return factory->GetScriptLoader() != nullptr; }));
+
+ // Simulate receiving an unsafe redirect (e.g. to a file scheme).
+ net::RedirectInfo redirect_info;
+ redirect_info.status_code = 302;
+ redirect_info.new_method = "GET";
+ redirect_info.new_url = GURL("file:///path/to/worker.js");
+ factory->GetScriptLoader()->OnReceiveRedirect(
+ redirect_info, network::mojom::URLResponseHead::New());
+ client.RunUntilComplete();
+
+ // Verify that the redirect was blocked and the load was aborted with
+ // ERR_UNSAFE_REDIRECT.
+ EXPECT_FALSE(client.has_received_redirect());
+ ASSERT_TRUE(client.has_received_completion());
+ EXPECT_EQ(net::ERR_UNSAFE_REDIRECT, client.completion_status().error_code);
+}
+
// TODO(falken): Add a test for a shared worker that's controlled by a service
// worker.
Regression Test / PoC
diff --git a/content/browser/worker_host/worker_script_loader_factory_unittest.cc b/content/browser/worker_host/worker_script_loader_factory_unittest.cc
index d9d896b..4746b25f 100644
--- a/content/browser/worker_host/worker_script_loader_factory_unittest.cc
+++ b/content/browser/worker_host/worker_script_loader_factory_unittest.cc
@@ -6,6 +6,7 @@
#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
+#include "base/test/run_until.h"
#include "content/browser/service_worker/embedded_worker_test_helper.h"
#include "content/browser/service_worker/service_worker_client.h"
#include "content/browser/service_worker/service_worker_context_core.h"
@@ -16,6 +17,7 @@
#include "content/test/fake_network_url_loader_factory.h"
#include "net/base/isolation_info.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "net/url_request/redirect_info.h"
#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/test/test_url_loader_client.h"
@@ -186,6 +188,80 @@
EXPECT_EQ(net::ERR_ABORTED, client.completion_status().error_code);
}
+// Tests that a redirect received while loading a blob: URL is rejected. Loading
+// a blob URL never produces a redirect, so any redirect must be rejected before
+// it is forwarded or followed.
+TEST_F(WorkerScriptLoaderFactoryTest, RedirectFromBlobUrl) {
+ GURL url("blob:https://www.example.com/49146318-7a89-4041-9bcc-36e6b6eeef86");
+
+ // Defer the mock network load so we can inject a redirect on the in-flight
+ // load.
+ network_loader_factory_instance_->DeferHandleRequest();
+
+ // Create the factory.
+ auto factory = std::make_unique<WorkerScriptLoaderFactory>(
+ kProcessId, DedicatedOrSharedWorkerToken(),
+ net::IsolationInfo::CreateForInternalRequest(url::Origin::Create(url)),
+ service_worker_handle_.get(), browser_context_getter_,
+ network_loader_factory_);
+
+ // Start loading the script.
+ network::TestURLLoaderClient client;
+ mojo::PendingRemote<network::mojom::URLLoader> loader =
+ CreateTestLoaderAndStart(url, factory.get(), &client);
+ ASSERT_TRUE(base::test::RunUntil(
+ [&]() { return factory->GetScriptLoader() != nullptr; }));
+
+ // Simulate receiving a redirect from the blob load.
+ net::RedirectInfo redirect_info;
+ redirect_info.status_code = 302;
+ redirect_info.new_method = "GET";
+ redirect_info.new_url = GURL("https://other.example.com/worker.js");
+ factory->GetScriptLoader()->OnReceiveRedirect(
+ redirect_info, network::mojom::URLResponseHead::New());
+ client.RunUntilComplete();
+
+ // Verify that the redirect was blocked and the load was aborted with
+ // ERR_UNSAFE_REDIRECT.
+ EXPECT_FALSE(client.has_received_redirect());
+ ASSERT_TRUE(client.has_received_completion());
+ EXPECT_EQ(net::ERR_UNSAFE_REDIRECT, client.completion_status().error_code);
+}
+
+// Tests that a redirect to an unsafe target scheme is rejected.
+TEST_F(WorkerScriptLoaderFactoryTest, RejectUnsafeRedirectTarget) {
+ GURL url("https://www.example.com/worker.js");
+
+ network_loader_factory_instance_->DeferHandleRequest();
+
+ auto factory = std::make_unique<WorkerScriptLoaderFactory>(
+ kProcessId, DedicatedOrSharedWorkerToken(),
+ net::IsolationInfo::CreateForInternalRequest(url::Origin::Create(url)),
+ service_worker_handle_.get(), browser_context_getter_,
+ network_loader_factory_);
+
+ network::TestURLLoaderClient client;
+ mojo::PendingRemote<network::mojom::URLLoader> loader =
+ CreateTestLoaderAndStart(url, factory.get(), &client);
+ ASSERT_TRUE(base::test::RunUntil(
+ [&]() { return factory->GetScriptLoader() != nullptr; }));
+
+ // Simulate receiving an unsafe redirect (e.g. to a file scheme).
+ net::RedirectInfo redirect_info;
+ redirect_info.status_code = 302;
+ redirect_info.new_method = "GET";
+ redirect_info.new_url = GURL("file:///path/to/worker.js");
+ factory->GetScriptLoader()->OnReceiveRedirect(
+ redirect_info, network::mojom::URLResponseHead::New());
+ client.RunUntilComplete();
+
+ // Verify that the redirect was blocked and the load was aborted with
+ // ERR_UNSAFE_REDIRECT.
+ EXPECT_FALSE(client.has_received_redirect());
+ ASSERT_TRUE(client.has_received_completion());
+ EXPECT_EQ(net::ERR_UNSAFE_REDIRECT, client.completion_status().error_code);
+}
+
// TODO(falken): Add a test for a shared worker that's controlled by a service
// worker.
Original Bug Report
Site Isolation Bypass via Missing Blob Redirect Validation in WorkerScriptLoader
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A missing URL scheme check in WorkerScriptLoader’s redirect handler potentially allows a compromised renderer to forge a cross-origin redirect from a custom blob: URL. By driving the browser-side service worker interceptor with the redirected victim URL, the renderer can potentially obtain cross-origin Service Worker response data. This leads to a potential Site Isolation bypass and cross-origin information leak.
Affected files:
content/browser/worker_host/worker_script_loader.cccontent/browser/worker_host/worker_script_fetcher.cc
Estimated timestamp from git blame: 2018-04-06
Root Cause
In Chromium, loading a blob: URL should never produce a redirect. For standard frame navigations, this security property is enforced in NavigationURLLoaderImpl::OnReceiveRedirect (content/browser/loader/navigation_url_loader_impl.cc:1595-1597):
if (url_.SchemeIsBlob()) {
// Loading a blob URL never produces a redirect.
error = net::ERR_UNSAFE_REDIRECT;
}
However, this mitigation was not applied to the worker main-script loading path. Specifically, WorkerScriptLoader::OnReceiveRedirect (content/browser/worker_host/worker_script_loader.cc:228-241) accepts redirects unconditionally:
void WorkerScriptLoader::OnReceiveRedirect(
const net::RedirectInfo& redirect_info,
network::mojom::URLResponseHeadPtr response_head) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (--redirect_limit_ == 0) { ...; return; }
redirect_info_ = redirect_info;
client_->OnReceiveRedirect(redirect_info, std::move(response_head));
}
When a worker script loader accepts a redirect, WorkerScriptFetcher::OnReceiveRedirect immediately auto-follows the redirect via url_loader_->FollowRedirect() (content/browser/worker_host/worker_script_fetcher.cc:784-791). This results in WorkerScriptLoader::FollowRedirect rewriting resource_request_.url with the new, attacker-supplied redirect URL and calling Start() again to re-enter the Service Worker interceptor layer with the new URL.
Because the request has not yet committed, the associated ServiceWorkerClient’s state can be mutated to the new cross-origin URL. The security helper CheckOnUpdateUrls (content/browser/service_worker/service_worker_security_utils.cc:54-77) that asserts origin boundaries is compiled out in production builds because it is wrapped entirely within #if DCHECK_IS_ON().
Consequently, the browser can be coerced into dispatching a FetchEvent to a cross-origin Service Worker (under the victim’s partitioned storage key) and routing the returned response data pipe back to the attacker’s renderer process via WorkerMainScriptLoadParams during client_->OnScriptLoadStarted in DedicatedWorkerHost::DidStartScriptLoad.
Potential Trigger Steps
Note: These are suggested and potential steps; our tooling does not have the ability to run functional exploit code to verify this sequence.
- A compromised renderer process under
https://evil.combinds a local implementation ofblink::mojom::Bloband registers a custom blob URL (e.g.,blob:https://evil.com/attacker-uuid) viaBlobURLStoreImpl::Register. - The renderer requests worker script loading for this blob URL via
DedicatedWorkerHostFactory::CreateWorkerHostAndStartScriptLoad, passing the correspondingBlobURLToken. - The browser establishes the connection and initiates the loader. Since the target is a blob URL, it queries the renderer’s blob implementation via
BlobURLLoaderFactory::CreateLoaderAndStart. This delivers a browser-process MojoURLLoaderClientremote directly to the attacker’s renderer. - The compromised renderer invokes
client->OnReceiveRedirecton the acquired Mojo remote, supplying a forgednet::RedirectInfotargetinghttps://victim.com/secret. WorkerScriptLoaderstores the redirect without validating that the original scheme wasblob:.WorkerScriptFetcherauto-follows, andWorkerScriptLoader::FollowRedirectupdates the request URL tohttps://victim.com/secretand restarts.- The browser-side
ServiceWorkerMainResourceLoaderInterceptorrunsInitializeForRequeston the redirected URL. Since no response has committed yet,is_response_committed()is false, allowing the browser to update the worker’sServiceWorkerClientURL and key to the victim’s origin (partitioned underhttps://evil.com). - The interceptor lookup succeeds for a partitioned Service Worker matching the victim’s origin, dispatches a
FetchEventto the victim’s Service Worker forhttps://victim.com/secret, and returns the cross-origin response data pipe directly back to the attacker’s renderer viaclient_->OnScriptLoadStarted.
Impact
A compromised renderer can potentially bypass Site Isolation and leak cross-origin data by forcing the browser to execute a cross-origin fetch through a victim’s partitioned Service Worker and retrieve its response body.
Suggested Fix
Enforce scheme validation inside WorkerScriptLoader::OnReceiveRedirect similar to the navigation loader. Reject any redirect attempt if the original request URL scheme is a blob URL:
void WorkerScriptLoader::OnReceiveRedirect(
const net::RedirectInfo& redirect_info,
network::mojom::URLResponseHeadPtr response_head) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (resource_request_.url.SchemeIsBlob()) {
complete_status_ = network::URLLoaderCompletionStatus(net::ERR_UNSAFE_REDIRECT);
CommitCompleted();
return;
}
if (--redirect_limit_ == 0) {
complete_status_ = network::URLLoaderCompletionStatus(net::ERR_TOO_MANY_REDIRECTS);
CommitCompleted();
return;
}
redirect_info_ = redirect_info;
client_->OnReceiveRedirect(redirect_info, std::move(response_head));
}
Evaluated with Chrome root at commit: 84065d9121f6e48f67755f0ae963cc09617e5c85
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.