CVE-2026-79025
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forcontent/browser/worker_host/worker_script_fetcher_unittest.cc |
modified | |
TEST_Fcontent/browser/worker_host/worker_script_loader_factory_unittest.cc |
modified |
Files Changed
content/browser/worker_host/worker_script_fetcher.cccontent/browser/worker_host/worker_script_fetcher_unittest.cccontent/browser/worker_host/worker_script_loader.cccontent/browser/worker_host/worker_script_loader_factory_unittest.cc
Patch
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");
Regression Test / PoC
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");
Original Bug Report
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.cccontent/browser/worker_host/dedicated_worker_host.cccontent/browser/worker_host/shared_worker_host.cccontent/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
- Blob URL Registration:
BlobURLStoreImpl::Registeraccepts an arbitrary renderer-suppliedblink.mojom::Blobremote and registers it in the browser-process registry (storage/browser/blob/blob_url_store_impl.cc:118). - Handing Client to Renderer: When a dedicated or shared worker main script is fetched from a Blob URL, the browser’s
WorkerScriptLoaderserves as theURLLoaderClient.BlobURLLoaderFactory::CreateLoaderAndStartforwards this client remote directly to the renderer-hostedBlobremote (storage/browser/blob/blob_url_loader_factory.cc:80-81). - Ungated OnReceiveResponse: The compromised renderer can invoke
URLLoaderClient::OnReceiveResponseon the browser-process client with a fully forgedURLResponseHead. UnlikeWorkerScriptLoader::OnReceiveRedirectwhich contains aSchemeIsBlob()gate to block unsafe transitions (content/browser/worker_host/worker_script_loader.cc:235),WorkerScriptLoader::OnReceiveResponsedoes not restrict or sanitize response parameters for blob URLs. - Downstream Spoofing: Because the renderer can forge fields such as
was_fetched_via_service_worker = true,client_address_space = kLoopback, andurl_list_via_service_worker = ["https://evil.com/forged"], the browser-sideDedicatedWorkerHostorSharedWorkerHostcalculates the worker’s security state based on these spoofed attributes. - LNA Bypass: Consequently, the browser mints a subresource
URLLoaderFactoryfor the worker whereip_address_spaceis set tokLoopback. This allows the renderer to query private network endpoints (e.g.,127.0.0.1or 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
- From a compromised renderer (e.g.,
https://evil.com), register a self-hostedblink.mojom.Blobreceiver mapping to a local Blob URL (e.g.,blob:https://evil.com/UUID). - Call
ResolveAsBlobURLTokento retrieve aBlobURLTokencorresponding to that URL. - Trigger dedicated worker creation by calling
DedicatedWorkerHostFactory.CreateWorkerHostAndStartScriptLoadwith the Blob URL and the token. - The custom renderer-hosted Blob implementation receives the
Load(..., client)call. On the provided browser-sideURLLoaderClientremote, callOnReceiveResponsewith a forgedURLResponseHeadwhere:url_list_via_service_worker = ["https://evil.com/forged"]was_fetched_via_service_worker = trueclient_address_space = kLoopbackparsed_headersis non-null withcross_origin_embedder_policy = kNone
- 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.