Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Workers
DescriptionInsufficient validation of untrusted input in Workers
ComponentWorkers
Bug ClassLogic Error
Tracker532939327
Fix commit22500d480098 (chromium/src) +94/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-06

Changed Functions

FunctionChangeNotes
if
content/browser/worker_host/worker_script_loader.cc
modified
TEST_F
content/browser/worker_host/worker_script_loader_factory_unittest.cc
modified

Files Changed

  • content/browser/worker_host/worker_script_loader.cc
  • content/browser/worker_host/worker_script_loader_factory_unittest.cc
From 22500d480098ce3bb02f8850a7e4f93bed85d335 Mon Sep 17 00:00:00 2001
From: Yoshisto Yanagisawa <[email protected]>
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 <[email protected]>
Auto-Submit: Yoshisato Yanagisawa <[email protected]>
Commit-Queue: Hiroki Nakagawa <[email protected]>
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.
 
Loading diff…

Regression Test / PoC

shipped with the fix
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.
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • content/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.

  1. A compromised renderer process under https://evil.com binds a local implementation of blink::mojom::Blob and registers a custom blob URL (e.g., blob:https://evil.com/attacker-uuid) via BlobURLStoreImpl::Register.
  2. The renderer requests worker script loading for this blob URL via DedicatedWorkerHostFactory::CreateWorkerHostAndStartScriptLoad, passing the corresponding BlobURLToken.
  3. 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 Mojo URLLoaderClient remote directly to the attacker’s renderer.
  4. The compromised renderer invokes client->OnReceiveRedirect on the acquired Mojo remote, supplying a forged net::RedirectInfo targeting https://victim.com/secret.
  5. WorkerScriptLoader stores the redirect without validating that the original scheme was blob:. WorkerScriptFetcher auto-follows, and WorkerScriptLoader::FollowRedirect updates the request URL to https://victim.com/secret and restarts.
  6. The browser-side ServiceWorkerMainResourceLoaderInterceptor runs InitializeForRequest on the redirected URL. Since no response has committed yet, is_response_committed() is false, allowing the browser to update the worker’s ServiceWorkerClient URL and key to the victim’s origin (partitioned under https://evil.com).
  7. The interceptor lookup succeeds for a partitioned Service Worker matching the victim’s origin, dispatches a FetchEvent to the victim’s Service Worker for https://victim.com/secret, and returns the cross-origin response data pipe directly back to the attacker’s renderer via client_->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.

View on issue tracker