Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Workers
DescriptionImproper input validation in Workers
ComponentWorkers
Bug ClassLogic Error
Tracker537846307
Fix commit5663e65f27a7 (chromium/src) +93/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
for
content/browser/worker_host/worker_script_fetcher_unittest.cc
modified
TEST_F
content/browser/worker_host/worker_script_loader_factory_unittest.cc
modified

Files Changed

  • content/browser/worker_host/worker_script_fetcher.cc
  • content/browser/worker_host/worker_script_fetcher_unittest.cc
  • content/browser/worker_host/worker_script_loader.cc
  • content/browser/worker_host/worker_script_loader_factory_unittest.cc
From 5663e65f27a7e6523ede473af83eb33efe4fd0d0 Mon Sep 17 00:00:00 2001
From: Yoshisto Yanagisawa <[email protected]>
Date: Mon, 27 Jul 2026 04:08:59 -0700
Subject: [PATCH] Sanitize response parameters for local worker script loads

Ensure response parameters received when loading local worker scripts (such as blob URLs) are properly sanitized and restricted to local defaults.

TAG=agy
CONV=d00718bc-39f5-483a-94a6-1bd6d20eab1f

Bug: 537846307
Change-Id: If1344306011bb4a2c3f5a4db0cc54fa2182acbf7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8142860
Reviewed-by: Hiroki Nakagawa <[email protected]>
Commit-Queue: Yoshisato Yanagisawa <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1668490}
---

diff --git a/content/browser/worker_host/worker_script_fetcher.cc b/content/browser/worker_host/worker_script_fetcher.cc
index db5a606..f49da48 100644
--- a/content/browser/worker_host/worker_script_fetcher.cc
+++ b/content/browser/worker_host/worker_script_fetcher.cc
@@ -690,6 +690,10 @@
     blink::mojom::WorkerMainScriptLoadParams* main_script_load_params) {
   DCHECK(main_script_load_params);
 
+  if (initial_request_url.SchemeIsLocal()) {
+    return initial_request_url;
+  }
+
   network::mojom::URLResponseHead* url_response_head =
       main_script_load_params->response_head.get();
 
diff --git a/content/browser/worker_host/worker_script_fetcher_unittest.cc b/content/browser/worker_host/worker_script_fetcher_unittest.cc
index 3b24e0c..b32dc0b 100644
--- a/content/browser/worker_host/worker_script_fetcher_unittest.cc
+++ b/content/browser/worker_host/worker_script_fetcher_unittest.cc
@@ -74,6 +74,12 @@
           {GURL("https://redirect_1.com"), GURL("https://redirect_2.com")},
           GURL("https://url_list_2.com"),
       },
+      {
+          GURL("blob:https://initial.com/uuid"),
+          {GURL("https://url_list_1.com"), GURL("https://url_list_2.com")},
+          {GURL("https://redirect_1.com"), GURL("https://redirect_2.com")},
+          GURL("blob:https://initial.com/uuid"),
+      },
   };
 
   for (const auto& test_case : kTestCases) {
diff --git a/content/browser/worker_host/worker_script_loader.cc b/content/browser/worker_host/worker_script_loader.cc
index dcccdae..5a9aa12 100644
--- a/content/browser/worker_host/worker_script_loader.cc
+++ b/content/browser/worker_host/worker_script_loader.cc
@@ -13,6 +13,7 @@
 #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/ip_endpoint.h"
 #include "net/base/load_timing_info.h"
 #include "net/base/net_errors.h"
 #include "net/url_request/redirect_util.h"
@@ -223,6 +224,25 @@
     mojo::ScopedDataPipeConsumerHandle body,
     std::optional<mojo_base::BigBuffer> cached_metadata) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  if (resource_request_.url.SchemeIsBlob() && response_head) {
+    // A blob URL is loaded by a renderer-hosted BlobURLLoader. A blob URL
+    // request is never handled by a service worker or a network socket.
+    // Sanitize service worker and network address space fields to ensure
+    // robustness against invalid input and maintain expected specifications.
+    response_head->was_fetched_via_service_worker = false;
+    response_head->url_list_via_service_worker.clear();
+    response_head->service_worker_response_source =
+        network::mojom::FetchResponseSource::kUnspecified;
+    response_head->initial_service_worker_status.reset();
+    response_head->service_worker_router_info.reset();
+    response_head->client_address_space =
+        network::mojom::IPAddressSpace::kUnknown;
+    response_head->response_address_space =
+        network::mojom::IPAddressSpace::kUnknown;
+    response_head->remote_endpoint = net::IPEndPoint();
+    response_head->was_fetched_via_cache = false;
+    response_head->is_validated = false;
+  }
   client_->OnReceiveResponse(std::move(response_head), std::move(body),
                              std::move(cached_metadata));
 }
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 4746b25f..0fa09e3 100644
--- a/content/browser/worker_host/worker_script_loader_factory_unittest.cc
+++ b/content/browser/worker_host/worker_script_loader_factory_unittest.cc
@@ -15,11 +15,14 @@
 #include "content/browser/worker_host/worker_script_loader.h"
 #include "content/public/test/browser_task_environment.h"
 #include "content/test/fake_network_url_loader_factory.h"
+#include "net/base/ip_address.h"
+#include "net/base/ip_endpoint.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/public/mojom/ip_address_space.mojom.h"
 #include "services/network/test/test_url_loader_client.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/common/tokens/tokens.h"
@@ -228,6 +231,66 @@
   EXPECT_EQ(net::ERR_UNSAFE_REDIRECT, client.completion_status().error_code);
 }
 
+// Tests that response headers received while loading a blob: URL are sanitized.
+// Loading a blob URL never produces service worker interception or network
+// address space metadata, so any unexpected fields must be reset before they
+// are forwarded to the client.
+TEST_F(WorkerScriptLoaderFactoryTest, ResponseFromBlobUrl) {
+  GURL url("blob:https://www.example.com/49146318-7a89-4041-9bcc-36e6b6eeef86");
+
+  // Defer the mock network load so we can inject a response 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 response from the blob load with unexpected fields.
+  auto response_head = network::mojom::URLResponseHead::New();
+  response_head->was_fetched_via_service_worker = true;
+  response_head->url_list_via_service_worker = {
+      GURL("https://other.example.com/worker.js")};
+  response_head->client_address_space =
+      network::mojom::IPAddressSpace::kLoopback;
+  response_head->response_address_space =
+      network::mojom::IPAddressSpace::kLoopback;
+  response_head->remote_endpoint =
+      net::IPEndPoint(net::IPAddress::IPv4Localhost(), 8080);
+  response_head->was_fetched_via_cache = true;
+  response_head->is_validated = true;
+
+  factory->GetScriptLoader()->OnReceiveResponse(
+      std::move(response_head), mojo::ScopedDataPipeConsumerHandle(),
+      std::nullopt);
+  client.RunUntilResponseReceived();
+
+  // Verify that the unexpected fields were sanitized.
+  ASSERT_TRUE(client.has_received_response());
+  EXPECT_FALSE(client.response_head()->was_fetched_via_service_worker);
+  EXPECT_TRUE(client.response_head()->url_list_via_service_worker.empty());
+  EXPECT_EQ(network::mojom::IPAddressSpace::kUnknown,
+            client.response_head()->client_address_space);
+  EXPECT_EQ(network::mojom::IPAddressSpace::kUnknown,
+            client.response_head()->response_address_space);
+  EXPECT_EQ(net::IPEndPoint(), client.response_head()->remote_endpoint);
+  factory->GetScriptLoader()->OnComplete(
+      network::URLLoaderCompletionStatus(net::OK));
+  factory->GetScriptLoader()->OnFetcherCallbackCalled();
+  client.RunUntilComplete();
+  EXPECT_EQ(net::OK, 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");
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/worker_host/worker_script_fetcher_unittest.cc b/content/browser/worker_host/worker_script_fetcher_unittest.cc
index 3b24e0c..b32dc0b 100644
--- a/content/browser/worker_host/worker_script_fetcher_unittest.cc
+++ b/content/browser/worker_host/worker_script_fetcher_unittest.cc
@@ -74,6 +74,12 @@
           {GURL("https://redirect_1.com"), GURL("https://redirect_2.com")},
           GURL("https://url_list_2.com"),
       },
+      {
+          GURL("blob:https://initial.com/uuid"),
+          {GURL("https://url_list_1.com"), GURL("https://url_list_2.com")},
+          {GURL("https://redirect_1.com"), GURL("https://redirect_2.com")},
+          GURL("blob:https://initial.com/uuid"),
+      },
   };
 
   for (const auto& test_case : kTestCases) {
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 4746b25f..0fa09e3 100644
--- a/content/browser/worker_host/worker_script_loader_factory_unittest.cc
+++ b/content/browser/worker_host/worker_script_loader_factory_unittest.cc
@@ -15,11 +15,14 @@
 #include "content/browser/worker_host/worker_script_loader.h"
 #include "content/public/test/browser_task_environment.h"
 #include "content/test/fake_network_url_loader_factory.h"
+#include "net/base/ip_address.h"
+#include "net/base/ip_endpoint.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/public/mojom/ip_address_space.mojom.h"
 #include "services/network/test/test_url_loader_client.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/common/tokens/tokens.h"
@@ -228,6 +231,66 @@
   EXPECT_EQ(net::ERR_UNSAFE_REDIRECT, client.completion_status().error_code);
 }
 
+// Tests that response headers received while loading a blob: URL are sanitized.
+// Loading a blob URL never produces service worker interception or network
+// address space metadata, so any unexpected fields must be reset before they
+// are forwarded to the client.
+TEST_F(WorkerScriptLoaderFactoryTest, ResponseFromBlobUrl) {
+  GURL url("blob:https://www.example.com/49146318-7a89-4041-9bcc-36e6b6eeef86");
+
+  // Defer the mock network load so we can inject a response 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 response from the blob load with unexpected fields.
+  auto response_head = network::mojom::URLResponseHead::New();
+  response_head->was_fetched_via_service_worker = true;
+  response_head->url_list_via_service_worker = {
+      GURL("https://other.example.com/worker.js")};
+  response_head->client_address_space =
+      network::mojom::IPAddressSpace::kLoopback;
+  response_head->response_address_space =
+      network::mojom::IPAddressSpace::kLoopback;
+  response_head->remote_endpoint =
+      net::IPEndPoint(net::IPAddress::IPv4Localhost(), 8080);
+  response_head->was_fetched_via_cache = true;
+  response_head->is_validated = true;
+
+  factory->GetScriptLoader()->OnReceiveResponse(
+      std::move(response_head), mojo::ScopedDataPipeConsumerHandle(),
+      std::nullopt);
+  client.RunUntilResponseReceived();
+
+  // Verify that the unexpected fields were sanitized.
+  ASSERT_TRUE(client.has_received_response());
+  EXPECT_FALSE(client.response_head()->was_fetched_via_service_worker);
+  EXPECT_TRUE(client.response_head()->url_list_via_service_worker.empty());
+  EXPECT_EQ(network::mojom::IPAddressSpace::kUnknown,
+            client.response_head()->client_address_space);
+  EXPECT_EQ(network::mojom::IPAddressSpace::kUnknown,
+            client.response_head()->response_address_space);
+  EXPECT_EQ(net::IPEndPoint(), client.response_head()->remote_endpoint);
+  factory->GetScriptLoader()->OnComplete(
+      network::URLLoaderCompletionStatus(net::OK));
+  factory->GetScriptLoader()->OnFetcherCallbackCalled();
+  client.RunUntilComplete();
+  EXPECT_EQ(net::OK, 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");
Loading diff…

Original Bug Report

reported by [email protected]

LNA Bypass via Forged URLResponseHead in WorkerScriptLoader with Blob URL Main Script

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 compromised renderer can potentially register a self-hosted Blob URL as a worker main script to forge a response head during script loading. Because WorkerScriptLoader::OnReceiveResponse lacks validation gating for blob schemes, the forged response propagates downstream. This allows the worker to be assigned a kLoopback address space, completely bypassing Local Network Access (LNA) protections.

Affected files:

  • content/browser/worker_host/worker_script_loader.cc
  • content/browser/worker_host/dedicated_worker_host.cc
  • content/browser/worker_host/shared_worker_host.cc
  • content/browser/worker_host/worker_script_fetcher.cc

Estimated timestamp from git blame: 2022-02-02

Description

There is a potential vulnerability in Chromium’s worker script loading infrastructure where a compromised renderer can bypass Local Network Access (LNA) protections. This occurs because WorkerScriptLoader::OnReceiveResponse does not validate or sanitize responses received from blob URL scheme loaders.

Root Cause Analysis

  1. Blob URL Registration: BlobURLStoreImpl::Register accepts an arbitrary renderer-supplied blink.mojom::Blob remote and registers it in the browser-process registry (storage/browser/blob/blob_url_store_impl.cc:118).
  2. Handing Client to Renderer: When a dedicated or shared worker main script is fetched from a Blob URL, the browser’s WorkerScriptLoader serves as the URLLoaderClient. BlobURLLoaderFactory::CreateLoaderAndStart forwards this client remote directly to the renderer-hosted Blob remote (storage/browser/blob/blob_url_loader_factory.cc:80-81).
  3. Ungated OnReceiveResponse: The compromised renderer can invoke URLLoaderClient::OnReceiveResponse on the browser-process client with a fully forged URLResponseHead. Unlike WorkerScriptLoader::OnReceiveRedirect which contains a SchemeIsBlob() gate to block unsafe transitions (content/browser/worker_host/worker_script_loader.cc:235), WorkerScriptLoader::OnReceiveResponse does not restrict or sanitize response parameters for blob URLs.
  4. Downstream Spoofing: Because the renderer can forge fields such as was_fetched_via_service_worker = true, client_address_space = kLoopback, and url_list_via_service_worker = ["https://evil.com/forged"], the browser-side DedicatedWorkerHost or SharedWorkerHost calculates the worker’s security state based on these spoofed attributes.
  5. LNA Bypass: Consequently, the browser mints a subresource URLLoaderFactory for the worker where ip_address_space is set to kLoopback. This allows the renderer to query private network endpoints (e.g., 127.0.0.1 or RFC1918 addresses) through the resulting factory without any LNA restrictions or permission prompts.

Note: These are suggested/potential steps of an exploit chain, as our tooling agent does not have the capability to run code or verify exploitability live.


Potential Steps to Trigger the Vulnerability

  1. From a compromised renderer (e.g., https://evil.com), register a self-hosted blink.mojom.Blob receiver mapping to a local Blob URL (e.g., blob:https://evil.com/UUID).
  2. Call ResolveAsBlobURLToken to retrieve a BlobURLToken corresponding to that URL.
  3. Trigger dedicated worker creation by calling DedicatedWorkerHostFactory.CreateWorkerHostAndStartScriptLoad with the Blob URL and the token.
  4. The custom renderer-hosted Blob implementation receives the Load(..., client) call. On the provided browser-side URLLoaderClient remote, call OnReceiveResponse with a forged URLResponseHead where:
    • url_list_via_service_worker = ["https://evil.com/forged"]
    • was_fetched_via_service_worker = true
    • client_address_space = kLoopback
    • parsed_headers is non-null with cross_origin_embedder_policy = kNone
  5. Retrieve the subresource factory delivered via the resulting worker client and use it to successfully issue requests to loopback/private network addresses (e.g., http://127.0.0.1:8000/) without triggering LNA restrictions.

Suggested Fix

To remediate this issue, restrict or sanitize the incoming URLResponseHead when the resource request scheme is a Blob URL. In WorkerScriptLoader::OnReceiveResponse (content/browser/worker_host/worker_script_loader.cc:221), verify if resource_request_.url.SchemeIsBlob() is true. If it is, either sanitize the response_head by clearing out renderer-influenceable fields (such as url_list_via_service_worker, was_fetched_via_service_worker, and client_address_space) or reject the response entirely if unexpected fields are populated.

Evaluated with Chrome root at commit: 5a99d0c5d2ec6c066f5131e7868ff637440cc3dc


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