CVE-2026-19153
Overview
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
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.