Medium chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Network
DescriptionIncorrect authorization in Network
ComponentNetwork
Sub-componentInternals>Network
Bug ClassAuthorization Error
Tracker537832408
Fix commite6f4a3b4803a (chromium/src) +224/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
EarlyHintsBlob
content/browser/blob_storage/blob_url_browsertest.cc
modified
TEST_F
content/browser/loader/navigation_early_hints_manager_unittest.cc
modified
TEST_F
content/browser/loader/navigation_url_loader_impl_unittest.cc
modified
TEST_F
content/browser/worker_host/worker_script_loader_factory_unittest.cc
modified

Files Changed

  • content/browser/loader/navigation_url_loader_impl_unittest.cc
  • content/browser/loader/navigation_url_loader_impl.cc
  • content/browser/worker_host/worker_script_loader.cc
  • content/browser/renderer_host/navigation_request.cc
  • content/browser/worker_host/worker_script_loader_factory_unittest.cc
  • content/browser/blob_storage/blob_url_browsertest.cc
  • content/browser/url_loader_factory_params_helper.cc
  • content/browser/loader/navigation_early_hints_manager.cc
  • content/browser/loader/navigation_url_loader_impl.h
  • content/browser/loader/navigation_early_hints_manager_unittest.cc
From e6f4a3b4803a7311519f0583cd6dec3cd26b7843 Mon Sep 17 00:00:00 2001
From: Kenichi Ishibashi <[email protected]>
Date: Mon, 07 Sep 2026 01:39:44 -0700
Subject: [PATCH] Validate scheme before processing Early Hints in NavigationURLLoader

HTTP 103 Early Hints are defined strictly for HTTP/1.1, HTTP/2, and
HTTP/3 connections. Non-HTTP(S) schemes, such as blob URLs, do not
support informational Early Hints responses. However,
NavigationURLLoaderImpl previously lacked a scheme check in
OnReceiveEarlyHints before delegating hints to the early hints manager.

This change adds a validation check ensuring that Early Hints are only
processed when the request URL has an HTTP or HTTPS scheme.
Informational responses received for other schemes are dropped, avoiding
invalid state configuration and DCHECK failures during navigation.

Bug: 537832408
Change-Id: I50ceeb847d0ed12c73706e69406d33069cf9cddc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8361852
Commit-Queue: Kenichi Ishibashi <[email protected]>
Reviewed-by: Rakina Zata Amni <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1693195}
---
diff --git a/content/browser/loader/navigation_early_hints_manager.cc b/content/browser/loader/navigation_early_hints_manager.cc
index 1d768f07..0c91211 100644
--- a/content/browser/loader/navigation_early_hints_manager.cc
+++ b/content/browser/loader/navigation_early_hints_manager.cc
@@ -404,6 +404,11 @@
 void NavigationEarlyHintsManager::HandleEarlyHints(
     network::mojom::EarlyHintsPtr early_hints,
     const network::ResourceRequest& request_for_navigation) {
+  // Only handle Early Hints for HTTP/HTTPS navigations.
+  if (!request_for_navigation.url.SchemeIsHTTPOrHTTPS()) {
+    return;
+  }
+
   // Ignore the second and subsequent responses to avoid situations where
   // policies such as CSP and connection allowlist are inconsistent among the
   // first and following responses. This behavior is specified by the step 19.5
diff --git a/content/browser/loader/navigation_url_loader_impl.cc b/content/browser/loader/navigation_url_loader_impl.cc
index bf56800..17b1557 100644
--- a/content/browser/loader/navigation_url_loader_impl.cc
+++ b/content/browser/loader/navigation_url_loader_impl.cc
@@ -1375,6 +1375,12 @@
 
 void NavigationURLLoaderImpl::OnReceiveEarlyHints(
     network::mojom::EarlyHintsPtr early_hints) {
+  // Early Hints is an HTTP protocol feature. Ignore Early Hints for
+  // non-HTTP/HTTPS URLs before inspecting renderer-influenced fields.
+  if (!url_.SchemeIsHTTPOrHTTPS()) {
+    return;
+  }
+
   // Early Hints should not come after actual response.
   DCHECK(!received_response_);
   DCHECK_NE(early_hints->ip_address_space,
diff --git a/content/browser/loader/navigation_url_loader_impl.h b/content/browser/loader/navigation_url_loader_impl.h
index de138f57..e72a516 100644
--- a/content/browser/loader/navigation_url_loader_impl.h
+++ b/content/browser/loader/navigation_url_loader_impl.h
@@ -124,6 +124,7 @@
 
   void TriggerTimeoutForTesting();
   const network::ResourceRequest& GetResourceRequestForTesting() const;
+  bool HasEarlyHintsManagerForTesting() const { return !!early_hints_manager_; }
 
  private:
   FRIEND_TEST_ALL_PREFIXES(NavigationURLLoaderImplTest,
diff --git a/content/browser/renderer_host/navigation_request.cc b/content/browser/renderer_host/navigation_request.cc
index a35f9ed..14d2911 100644
--- a/content/browser/renderer_host/navigation_request.cc
+++ b/content/browser/renderer_host/navigation_request.cc
@@ -5605,6 +5605,11 @@
   CHECK(IsInMainFrame());
   CHECK(!IsPageActivation());
 
+  // Early Hints are only valid for HTTP/HTTPS navigations.
+  if (!GetURL().SchemeIsHTTPOrHTTPS()) {
+    return std::nullopt;
+  }
+
   // Getting a RenderProcessHost from a tentative RenderFrameHost during
   // navigation is generally discouraged because it has potential performance
   // impact (the RenderProcessHost could be discarded without actually being
diff --git a/content/browser/url_loader_factory_params_helper.cc b/content/browser/url_loader_factory_params_helper.cc
index c8e7b765..f016c5e 100644
--- a/content/browser/url_loader_factory_params_helper.cc
+++ b/content/browser/url_loader_factory_params_helper.cc
@@ -370,6 +370,8 @@
         shared_dictionary_observer,
     mojo::PendingRemote<network::mojom::DeviceBoundSessionAccessObserver>
         device_bound_session_observer) {
+  CHECK(navigation_request.GetURL().SchemeIsHTTPOrHTTPS());
+
   // TODO(crbug.com/40188470): Consider not using the speculative
   // RenderFrameHostImpl to create URLLoaderNetworkServiceObserver.
   // In general we should avoid using speculative RenderFrameHostImpl
diff --git a/content/browser/worker_host/worker_script_loader.cc b/content/browser/worker_host/worker_script_loader.cc
index 5a9aa12..c0da3fa 100644
--- a/content/browser/worker_host/worker_script_loader.cc
+++ b/content/browser/worker_host/worker_script_loader.cc
@@ -216,6 +216,10 @@
 void WorkerScriptLoader::OnReceiveEarlyHints(
     network::mojom::EarlyHintsPtr early_hints) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  // Early Hints are only supported for HTTP/HTTPS requests.
+  if (!resource_request_.url.SchemeIsHTTPOrHTTPS()) {
+    return;
+  }
   client_->OnReceiveEarlyHints(std::move(early_hints));
 }
 
diff --git a/content/browser/blob_storage/blob_url_browsertest.cc b/content/browser/blob_storage/blob_url_browsertest.cc
index 22a4356d..9cca5d3 100644
--- a/content/browser/blob_storage/blob_url_browsertest.cc
+++ b/content/browser/blob_storage/blob_url_browsertest.cc
@@ -37,6 +37,8 @@
 #include "net/http/http_response_headers.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
 #include "net/url_request/redirect_info.h"
+#include "services/network/public/mojom/early_hints.mojom.h"
+#include "services/network/public/mojom/link_header.mojom.h"
 #include "services/network/public/mojom/url_loader.mojom.h"
 #include "services/network/public/mojom/url_response_head.mojom.h"
 #include "storage/browser/blob/blob_url_registry.h"
@@ -308,6 +310,93 @@
   mojo::Remote<network::mojom::URLLoaderClient> client_;
 };
 
+// A blink::mojom::Blob implementation that, when Load() is called, sends
+// Early Hints before sending the response body.
+class EarlyHintsBlob : public blink::mojom::Blob {
+ public:
+  explicit EarlyHintsBlob(const GURL& preload_target)
+      : preload_target_(preload_target) {}
+
+  mojo::PendingRemote<blink::mojom::Blob> BindNewPipeAndPassRemote() {
+    mojo::PendingRemote<blink::mojom::Blob> remote;
+    receivers_.Add(this, remote.InitWithNewPipeAndPassReceiver());
+    return remote;
+  }
+
+  // blink::mojom::Blob:
+  void Clone(mojo::PendingReceiver<blink::mojom::Blob> receiver) override {
+    receivers_.Add(this, std::move(receiver));
+  }
+  void AsDataPipeGetter(
+      mojo::PendingReceiver<network::mojom::DataPipeGetter>) override {
+    NOTREACHED();
+  }
+  void ReadAll(mojo::ScopedDataPipeProducerHandle,
+               mojo::PendingRemote<blink::mojom::BlobReaderClient>) override {
+    NOTREACHED();
+  }
+  void ReadRange(uint64_t,
+                 uint64_t,
+                 mojo::ScopedDataPipeProducerHandle,
+                 mojo::PendingRemote<blink::mojom::BlobReaderClient>) override {
+    NOTREACHED();
+  }
+  void Load(
+      mojo::PendingReceiver<network::mojom::URLLoader> loader,
+      const std::string& method,
+      const net::HttpRequestHeaders&,
+      mojo::PendingRemote<network::mojom::URLLoaderClient> client) override {
+    loader_receiver_ = std::move(loader);
+    client_.reset();
+    client_.Bind(std::move(client));
+
+    auto hints = network::mojom::EarlyHints::New();
+    hints->referrer_policy = network::mojom::ReferrerPolicy::kNever;
+    hints->ip_address_space = network::mojom::IPAddressSpace::kLoopback;
+    hints->headers = network::mojom::ParsedHeaders::New();
+    auto link = network::mojom::LinkHeader::New();
+    link->href = preload_target_;
+    link->rel = network::mojom::LinkRelAttribute::kPreload;
+    link->as = network::mojom::LinkAsAttribute::kScript;
+    hints->headers->link_headers.push_back(std::move(link));
+    client_->OnReceiveEarlyHints(std::move(hints));
+
+    std::string body = "<html><body>sample_blob_content</body></html>";
+    mojo::ScopedDataPipeProducerHandle producer_handle;
+    mojo::ScopedDataPipeConsumerHandle consumer_handle;
+    CHECK_EQ(MOJO_RESULT_OK,
+             mojo::CreateDataPipe(nullptr, producer_handle, consumer_handle));
+    size_t actually_written_bytes = 0;
+    CHECK_EQ(MOJO_RESULT_OK,
+             producer_handle->WriteData(base::as_byte_span(body),
+                                        MOJO_WRITE_DATA_FLAG_NONE,
+                                        actually_written_bytes));
+    CHECK_EQ(actually_written_bytes, body.size());
+
+    auto head = network::mojom::URLResponseHead::New();
+    head->headers = net::HttpResponseHeaders::TryToCreate(
+        "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n");
+    head->mime_type = "text/html";
+    client_->OnReceiveResponse(std::move(head), std::move(consumer_handle),
+                               std::nullopt);
+    client_->OnComplete(network::URLLoaderCompletionStatus(net::OK));
+  }
+  void ReadSideData(ReadSideDataCallback) override { NOTREACHED(); }
+  void CaptureSnapshot(CaptureSnapshotCallback callback) override {
+    std::move(callback).Run(0, std::nullopt);
+  }
+  void GetInternalUUID(GetInternalUUIDCallback callback) override {
+    std::move(callback).Run("");
+  }
+
+ private:
+  const GURL preload_target_;
+  mojo::ReceiverSet<blink::mojom::Blob> receivers_;
+  // Retain the receiver so the endpoint does not signal disconnect.
+  mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver_;
+  mojo::Remote<network::mojom::URLLoaderClient> client_;
+};
+
 }  // namespace
 
 // A blob never serves a redirect, so a navigation to a blob URL whose
@@ -342,6 +431,36 @@
   EXPECT_NE(redirect_target, shell()->web_contents()->GetLastCommittedURL());
 }
 
+// A blob URL navigation should ignore Early Hints.
+IN_PROC_BROWSER_TEST_F(BlobUrlBrowserTest,
+                       NavigationToBlobUrlIgnoresEarlyHints) {
+  GURL url = embedded_test_server()->GetURL("a.test", "/title1.html");
+  url::Origin origin = url::Origin::Create(url);
+  EXPECT_TRUE(NavigateToURL(shell(), url));
+
+  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
+      shell()->web_contents()->GetPrimaryMainFrame());
+
+  const GURL preload_target =
+      embedded_test_server()->GetURL("a.test", "/title2.html");
+  EarlyHintsBlob blob(preload_target);
+
+  const GURL blob_url("blob:" + origin.Serialize() +
+                      "/33221100-0000-0000-0000-000000000001");
+  static_cast<StoragePartitionImpl*>(rfh->GetStoragePartition())
+      ->GetBlobUrlRegistry()
+      ->AddUrlMapping(blob_url, blob.BindNewPipeAndPassRemote(),
+                      blink::StorageKey::CreateFirstParty(origin), origin,
+                      rfh->GetProcess()->GetDeprecatedID());
+
+  EXPECT_TRUE(NavigateToURL(shell(), blob_url));
+  RenderFrameHostImpl* new_rfh = static_cast<RenderFrameHostImpl*>(
+      shell()->web_contents()->GetPrimaryMainFrame());
+  EXPECT_EQ(new_rfh->early_hints_manager(), nullptr);
+  EXPECT_EQ("sample_blob_content",
+            EvalJs(shell()->web_contents(), "document.body.innerText"));
+}
+
 IN_PROC_BROWSER_TEST_F(BlobUrlBrowserTest,
                        TestUseCounterForCrossPartitionSameOriginBlobURLFetch) {
   GURL main_url = embedded_test_server()->GetURL(
diff --git a/content/browser/loader/navigation_early_hints_manager_unittest.cc b/content/browser/loader/navigation_early_hints_manager_unittest.cc
index e425c63..4694327 100644
--- a/content/browser/loader/navigation_early_hints_manager_unittest.cc
+++ b/content/browser/loader/navigation_early_hints_manager_unittest.cc
@@ -272,6 +272,23 @@
   EXPECT_FALSE(early_hints_manager().HasInflightPreloads());
 }
 
+TEST_F(NavigationEarlyHintsManagerTest, NonHttpNavigationUrlIgnored) {
+  for (const char* url_str :
+       {"blob:https://a.test/1234", "filesystem:https://a.test/temporary/1234",
+        "data:text/html,sample_data", "file:///test/path", "about:blank"}) {
+    network::ResourceRequest request;
+    request.is_outermost_main_frame = true;
+    request.url = GURL(url_str);
+
+    early_hints_manager().HandleEarlyHints(CreateEarlyHintWithPreload(),
+                                           request);
+
+    EXPECT_FALSE(early_hints_manager().WasResourceHintsReceived());
+    EXPECT_FALSE(early_hints_manager().HasInflightPreloads());
+    EXPECT_TRUE(fake_network_context().preconnect_requests().empty());
+  }
+}
+
 TEST_F(NavigationEarlyHintsManagerTest, SinglePreconnect) {
   auto preconnect_url = GURL("https://b.test");
   auto link_header = network::mojom::LinkHeader::New(
diff --git a/content/browser/loader/navigation_url_loader_impl_unittest.cc b/content/browser/loader/navigation_url_loader_impl_unittest.cc
index df925305..059f589 100644
--- a/content/browser/loader/navigation_url_loader_impl_unittest.cc
+++ b/content/browser/loader/navigation_url_loader_impl_unittest.cc
@@ -56,7 +56,9 @@
 #include "services/network/public/cpp/permissions_policy/permissions_policy.h"
 #include "services/network/public/cpp/single_request_url_loader_factory.h"
 #include "services/network/public/cpp/url_loader_completion_status.h"
+#include "services/network/public/mojom/early_hints.mojom.h"
 #include "services/network/public/mojom/fetch_api.mojom.h"
+#include "services/network/public/mojom/link_header.mojom.h"
 #include "services/network/resource_scheduler/resource_scheduler_client.h"
 #include "services/network/test/url_loader_context_for_tests.h"
 #include "services/network/url_loader.h"
@@ -1787,4 +1789,31 @@
             net::StorageAccessApiStatus::kNone);
 }
 
+TEST_F(NavigationURLLoaderImplTest, EarlyHintsIgnoredForNonHttpSchemes) {
+  TestNavigationURLLoaderDelegate delegate;
+  for (const char* url_str :
+       {"blob:https://example.com/00000000-0000-0000-0000-000000000000",
+        "filesystem:https://example.com/temporary/test",
+        "data:text/html,sample_data", "file:///test/path", "about:blank"}) {
+    auto loader =
+        CreateTestLoader(GURL(url_str), std::string(), "GET", &delegate);
+
+    auto link_header = network::mojom::LinkHeader::New(
+        GURL("https://example.com/script.js"),
+        network::mojom::LinkRelAttribute::kPreload,
+        network::mojom::LinkAsAttribute::kScript,
+        network::mojom::CrossOriginAttribute::kUnspecified,
+        network::mojom::FetchPriorityAttribute::kAuto,
+        /*mime_type=*/std::nullopt);
+    auto hints = network::mojom::EarlyHints::New();
+    hints->headers = network::mojom::ParsedHeaders::New();
+    hints->headers->link_headers.push_back(std::move(link_header));
+    hints->ip_address_space = network::mojom::IPAddressSpace::kLoopback;
+
+    static_cast<network::mojom::URLLoaderClient*>(loader.get())
+        ->OnReceiveEarlyHints(std::move(hints));
+    EXPECT_FALSE(loader->HasEarlyHintsManagerForTesting());
+  }
+}
+
 }  // namespace content
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 0fa09e3..450fdf6 100644
--- a/content/browser/worker_host/worker_script_loader_factory_unittest.cc
+++ b/content/browser/worker_host/worker_script_loader_factory_unittest.cc
@@ -21,6 +21,7 @@
 #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/early_hints.mojom.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"
@@ -291,6 +292,41 @@
   EXPECT_EQ(net::OK, client.completion_status().error_code);
 }
 
+// Tests that Early Hints received while loading a blob: URL are ignored.
+TEST_F(WorkerScriptLoaderFactoryTest, EarlyHintsFromBlobUrl) {
+  GURL url("blob:https://www.example.com/49146318-7a89-4041-9bcc-36e6b6eeef86");
+
+  // Defer the mock network load so we can inject an early hint 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; }));
+
+  auto hints = network::mojom::EarlyHints::New();
+  hints->headers = network::mojom::ParsedHeaders::New();
+  hints->ip_address_space = network::mojom::IPAddressSpace::kLoopback;
+  factory->GetScriptLoader()->OnReceiveEarlyHints(std::move(hints));
+  factory->GetScriptLoader()->OnComplete(
+      network::URLLoaderCompletionStatus(net::OK));
+  factory->GetScriptLoader()->OnFetcherCallbackCalled();
+  client.RunUntilComplete();
+
+  EXPECT_FALSE(client.has_received_early_hints());
+  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/blob_storage/blob_url_browsertest.cc b/content/browser/blob_storage/blob_url_browsertest.cc
index 22a4356d..9cca5d3 100644
--- a/content/browser/blob_storage/blob_url_browsertest.cc
+++ b/content/browser/blob_storage/blob_url_browsertest.cc
@@ -37,6 +37,8 @@
 #include "net/http/http_response_headers.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
 #include "net/url_request/redirect_info.h"
+#include "services/network/public/mojom/early_hints.mojom.h"
+#include "services/network/public/mojom/link_header.mojom.h"
 #include "services/network/public/mojom/url_loader.mojom.h"
 #include "services/network/public/mojom/url_response_head.mojom.h"
 #include "storage/browser/blob/blob_url_registry.h"
@@ -308,6 +310,93 @@
   mojo::Remote<network::mojom::URLLoaderClient> client_;
 };
 
+// A blink::mojom::Blob implementation that, when Load() is called, sends
+// Early Hints before sending the response body.
+class EarlyHintsBlob : public blink::mojom::Blob {
+ public:
+  explicit EarlyHintsBlob(const GURL& preload_target)
+      : preload_target_(preload_target) {}
+
+  mojo::PendingRemote<blink::mojom::Blob> BindNewPipeAndPassRemote() {
+    mojo::PendingRemote<blink::mojom::Blob> remote;
+    receivers_.Add(this, remote.InitWithNewPipeAndPassReceiver());
+    return remote;
+  }
+
+  // blink::mojom::Blob:
+  void Clone(mojo::PendingReceiver<blink::mojom::Blob> receiver) override {
+    receivers_.Add(this, std::move(receiver));
+  }
+  void AsDataPipeGetter(
+      mojo::PendingReceiver<network::mojom::DataPipeGetter>) override {
+    NOTREACHED();
+  }
+  void ReadAll(mojo::ScopedDataPipeProducerHandle,
+               mojo::PendingRemote<blink::mojom::BlobReaderClient>) override {
+    NOTREACHED();
+  }
+  void ReadRange(uint64_t,
+                 uint64_t,
+                 mojo::ScopedDataPipeProducerHandle,
+                 mojo::PendingRemote<blink::mojom::BlobReaderClient>) override {
+    NOTREACHED();
+  }
+  void Load(
+      mojo::PendingReceiver<network::mojom::URLLoader> loader,
+      const std::string& method,
+      const net::HttpRequestHeaders&,
+      mojo::PendingRemote<network::mojom::URLLoaderClient> client) override {
+    loader_receiver_ = std::move(loader);
+    client_.reset();
+    client_.Bind(std::move(client));
+
+    auto hints = network::mojom::EarlyHints::New();
+    hints->referrer_policy = network::mojom::ReferrerPolicy::kNever;
+    hints->ip_address_space = network::mojom::IPAddressSpace::kLoopback;
+    hints->headers = network::mojom::ParsedHeaders::New();
+    auto link = network::mojom::LinkHeader::New();
+    link->href = preload_target_;
+    link->rel = network::mojom::LinkRelAttribute::kPreload;
+    link->as = network::mojom::LinkAsAttribute::kScript;
+    hints->headers->link_headers.push_back(std::move(link));
+    client_->OnReceiveEarlyHints(std::move(hints));
+
+    std::string body = "<html><body>sample_blob_content</body></html>";
+    mojo::ScopedDataPipeProducerHandle producer_handle;
+    mojo::ScopedDataPipeConsumerHandle consumer_handle;
+    CHECK_EQ(MOJO_RESULT_OK,
+             mojo::CreateDataPipe(nullptr, producer_handle, consumer_handle));
+    size_t actually_written_bytes = 0;
+    CHECK_EQ(MOJO_RESULT_OK,
+             producer_handle->WriteData(base::as_byte_span(body),
+                                        MOJO_WRITE_DATA_FLAG_NONE,
+                                        actually_written_bytes));
+    CHECK_EQ(actually_written_bytes, body.size());
+
+    auto head = network::mojom::URLResponseHead::New();
+    head->headers = net::HttpResponseHeaders::TryToCreate(
+        "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n");
+    head->mime_type = "text/html";
+    client_->OnReceiveResponse(std::move(head), std::move(consumer_handle),
+                               std::nullopt);
+    client_->OnComplete(network::URLLoaderCompletionStatus(net::OK));
+  }
+  void ReadSideData(ReadSideDataCallback) override { NOTREACHED(); }
+  void CaptureSnapshot(CaptureSnapshotCallback callback) override {
+    std::move(callback).Run(0, std::nullopt);
+  }
+  void GetInternalUUID(GetInternalUUIDCallback callback) override {
+    std::move(callback).Run("");
+  }
+
+ private:
+  const GURL preload_target_;
+  mojo::ReceiverSet<blink::mojom::Blob> receivers_;
+  // Retain the receiver so the endpoint does not signal disconnect.
+  mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver_;
+  mojo::Remote<network::mojom::URLLoaderClient> client_;
+};
+
 }  // namespace
 
 // A blob never serves a redirect, so a navigation to a blob URL whose
@@ -342,6 +431,36 @@
   EXPECT_NE(redirect_target, shell()->web_contents()->GetLastCommittedURL());
 }
 
+// A blob URL navigation should ignore Early Hints.
+IN_PROC_BROWSER_TEST_F(BlobUrlBrowserTest,
+                       NavigationToBlobUrlIgnoresEarlyHints) {
+  GURL url = embedded_test_server()->GetURL("a.test", "/title1.html");
+  url::Origin origin = url::Origin::Create(url);
+  EXPECT_TRUE(NavigateToURL(shell(), url));
+
+  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
+      shell()->web_contents()->GetPrimaryMainFrame());
+
+  const GURL preload_target =
+      embedded_test_server()->GetURL("a.test", "/title2.html");
+  EarlyHintsBlob blob(preload_target);
+
+  const GURL blob_url("blob:" + origin.Serialize() +
+                      "/33221100-0000-0000-0000-000000000001");
+  static_cast<StoragePartitionImpl*>(rfh->GetStoragePartition())
+      ->GetBlobUrlRegistry()
+      ->AddUrlMapping(blob_url, blob.BindNewPipeAndPassRemote(),
+                      blink::StorageKey::CreateFirstParty(origin), origin,
+                      rfh->GetProcess()->GetDeprecatedID());
+
+  EXPECT_TRUE(NavigateToURL(shell(), blob_url));
+  RenderFrameHostImpl* new_rfh = static_cast<RenderFrameHostImpl*>(
+      shell()->web_contents()->GetPrimaryMainFrame());
+  EXPECT_EQ(new_rfh->early_hints_manager(), nullptr);
+  EXPECT_EQ("sample_blob_content",
+            EvalJs(shell()->web_contents(), "document.body.innerText"));
+}
+
 IN_PROC_BROWSER_TEST_F(BlobUrlBrowserTest,
                        TestUseCounterForCrossPartitionSameOriginBlobURLFetch) {
   GURL main_url = embedded_test_server()->GetURL(
diff --git a/content/browser/loader/navigation_early_hints_manager_unittest.cc b/content/browser/loader/navigation_early_hints_manager_unittest.cc
index e425c63..4694327 100644
--- a/content/browser/loader/navigation_early_hints_manager_unittest.cc
+++ b/content/browser/loader/navigation_early_hints_manager_unittest.cc
@@ -272,6 +272,23 @@
   EXPECT_FALSE(early_hints_manager().HasInflightPreloads());
 }
 
+TEST_F(NavigationEarlyHintsManagerTest, NonHttpNavigationUrlIgnored) {
+  for (const char* url_str :
+       {"blob:https://a.test/1234", "filesystem:https://a.test/temporary/1234",
+        "data:text/html,sample_data", "file:///test/path", "about:blank"}) {
+    network::ResourceRequest request;
+    request.is_outermost_main_frame = true;
+    request.url = GURL(url_str);
+
+    early_hints_manager().HandleEarlyHints(CreateEarlyHintWithPreload(),
+                                           request);
+
+    EXPECT_FALSE(early_hints_manager().WasResourceHintsReceived());
+    EXPECT_FALSE(early_hints_manager().HasInflightPreloads());
+    EXPECT_TRUE(fake_network_context().preconnect_requests().empty());
+  }
+}
+
 TEST_F(NavigationEarlyHintsManagerTest, SinglePreconnect) {
   auto preconnect_url = GURL("https://b.test");
   auto link_header = network::mojom::LinkHeader::New(
diff --git a/content/browser/loader/navigation_url_loader_impl_unittest.cc b/content/browser/loader/navigation_url_loader_impl_unittest.cc
index df925305..059f589 100644
--- a/content/browser/loader/navigation_url_loader_impl_unittest.cc
+++ b/content/browser/loader/navigation_url_loader_impl_unittest.cc
@@ -56,7 +56,9 @@
 #include "services/network/public/cpp/permissions_policy/permissions_policy.h"
 #include "services/network/public/cpp/single_request_url_loader_factory.h"
 #include "services/network/public/cpp/url_loader_completion_status.h"
+#include "services/network/public/mojom/early_hints.mojom.h"
 #include "services/network/public/mojom/fetch_api.mojom.h"
+#include "services/network/public/mojom/link_header.mojom.h"
 #include "services/network/resource_scheduler/resource_scheduler_client.h"
 #include "services/network/test/url_loader_context_for_tests.h"
 #include "services/network/url_loader.h"
@@ -1787,4 +1789,31 @@
             net::StorageAccessApiStatus::kNone);
 }
 
+TEST_F(NavigationURLLoaderImplTest, EarlyHintsIgnoredForNonHttpSchemes) {
+  TestNavigationURLLoaderDelegate delegate;
+  for (const char* url_str :
+       {"blob:https://example.com/00000000-0000-0000-0000-000000000000",
+        "filesystem:https://example.com/temporary/test",
+        "data:text/html,sample_data", "file:///test/path", "about:blank"}) {
+    auto loader =
+        CreateTestLoader(GURL(url_str), std::string(), "GET", &delegate);
+
+    auto link_header = network::mojom::LinkHeader::New(
+        GURL("https://example.com/script.js"),
+        network::mojom::LinkRelAttribute::kPreload,
+        network::mojom::LinkAsAttribute::kScript,
+        network::mojom::CrossOriginAttribute::kUnspecified,
+        network::mojom::FetchPriorityAttribute::kAuto,
+        /*mime_type=*/std::nullopt);
+    auto hints = network::mojom::EarlyHints::New();
+    hints->headers = network::mojom::ParsedHeaders::New();
+    hints->headers->link_headers.push_back(std::move(link_header));
+    hints->ip_address_space = network::mojom::IPAddressSpace::kLoopback;
+
+    static_cast<network::mojom::URLLoaderClient*>(loader.get())
+        ->OnReceiveEarlyHints(std::move(hints));
+    EXPECT_FALSE(loader->HasEarlyHintsManagerForTesting());
+  }
+}
+
 }  // namespace content
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 0fa09e3..450fdf6 100644
--- a/content/browser/worker_host/worker_script_loader_factory_unittest.cc
+++ b/content/browser/worker_host/worker_script_loader_factory_unittest.cc
@@ -21,6 +21,7 @@
 #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/early_hints.mojom.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"
@@ -291,6 +292,41 @@
   EXPECT_EQ(net::OK, client.completion_status().error_code);
 }
 
+// Tests that Early Hints received while loading a blob: URL are ignored.
+TEST_F(WorkerScriptLoaderFactoryTest, EarlyHintsFromBlobUrl) {
+  GURL url("blob:https://www.example.com/49146318-7a89-4041-9bcc-36e6b6eeef86");
+
+  // Defer the mock network load so we can inject an early hint 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; }));
+
+  auto hints = network::mojom::EarlyHints::New();
+  hints->headers = network::mojom::ParsedHeaders::New();
+  hints->ip_address_space = network::mojom::IPAddressSpace::kLoopback;
+  factory->GetScriptLoader()->OnReceiveEarlyHints(std::move(hints));
+  factory->GetScriptLoader()->OnComplete(
+      network::URLLoaderCompletionStatus(net::OK));
+  factory->GetScriptLoader()->OnFetcherCallbackCalled();
+  client.RunUntilComplete();
+
+  EXPECT_FALSE(client.has_received_early_hints());
+  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]

Bypass of Local Network Access via forged EarlyHints in blob URL navigation

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 bypass Local Network Access (LNA) restrictions during top-level blob URL navigations. Due to a missing scheme check in NavigationURLLoaderImpl::OnReceiveEarlyHints, a renderer-hosted blob can supply forged EarlyHints to configure a browser-minted URLLoaderFactory with a loopback address space, potentially allowing LNA-bypassing preloads to private network targets.

Affected files:

  • content/browser/loader/navigation_url_loader_impl.cc
  • content/browser/loader/navigation_early_hints_manager.cc
  • content/browser/renderer_host/navigation_request.cc
  • content/browser/url_loader_factory_params_helper.cc
  • storage/browser/blob/blob_url_store_impl.cc

Estimated timestamp from git blame: 2021-07-16

Root Cause

NavigationURLLoaderImpl::OnReceiveEarlyHints accepts an EarlyHints struct on a URLLoaderClient remote. For blob: navigations, this remote is held by the renderer. However, unlike the sibling OnReceiveRedirect callback, OnReceiveEarlyHints performs no SchemeIsBlob() check to reject early hints when loading a blob URL.

Compare the unfixed callback:

// content/browser/loader/navigation_url_loader_impl.cc:1381
void NavigationURLLoaderImpl::OnReceiveEarlyHints(
    network::mojom::EarlyHintsPtr early_hints) {
  DCHECK(!received_response_);
  DCHECK_NE(early_hints->ip_address_space,
            network::mojom::IPAddressSpace::kUnknown);
  ...
  if (frame_tree_node->GetParentOrOuterDocument()) return;
  if (!early_hints_manager_) {
    std::optional<NavigationEarlyHintsManagerParams> params =
        delegate_->CreateNavigationEarlyHintsManagerParams(*early_hints);
    ...
  }
  early_hints_manager_->HandleEarlyHints(std::move(early_hints), resource_request());
}

Against the sister callback redirect check:

// content/browser/loader/navigation_url_loader_impl.cc:1581
if (url_.SchemeIsBlob()) {
  // Loading a blob URL never produces a redirect.
  error = net::ERR_UNSAFE_REDIRECT;
}

When a blob: URL navigation starts, BlobURLLoaderFactory::CreateLoaderAndStart delegates the load directly to the renderer-hosted mock Blob remote: blob_->Load(..., std::move(client)) (storage/browser/blob/blob_url_loader_factory.cc:80). This hands the browser-side URLLoaderClient directly to the renderer. A compromised renderer can then call client->OnReceiveEarlyHints with an arbitrary ip_address_space (e.g., kLoopback), bypassing the standard Private/Local Network Access protections.

Potential Attack Scenario

Note: These are suggested/potential steps; our tooling agent does not yet have the ability to run code, and we do not have a working proof of concept that has been successfully run.

  1. A compromised renderer registers a custom blink::mojom::Blob remote representing blob:https://evil.com/UUID using BlobURLStoreImpl::Register (storage/browser/blob/blob_url_store_impl.cc:118).
  2. The renderer requests a navigation token and initiates a top-level main-frame navigation to blob:https://evil.com/UUID (e.g., via window.open).
  3. During load initiation, BlobURLLoaderFactory::CreateLoaderAndStart passes the browser-owned URLLoaderClient to the renderer’s mock Blob remote via blob_->Load(..., client) (storage/browser/blob/blob_url_loader_factory.cc:80).
  4. The renderer calls client->OnReceiveEarlyHints(...) over the Mojo pipe, passing ip_address_space = kLoopback and setting a Link header for a preload request to a target local network IP (e.g., http://192.168.1.1/admin/reboot).
  5. The browser process receives the forged early hints and processes them because there is no scheme validation for blob: URLs in OnReceiveEarlyHints.
  6. The browser mints an EarlyHints preloading URLLoaderFactory with ClientSecurityState.ip_address_space = kLoopback (content/browser/url_loader_factory_params_helper.cc:397).
  7. The browser issues the preload GET request through the Network Service. The LocalNetworkAccessChecker collapses both the target space (kLocal) and the client space (kLoopback -> kLocal), evaluates that the target is not “less public” than the client, and returns kAllowedNoLessPublic (services/network/local_network_access_checker.cc:157), fully bypassing LNA checks for the preloaded resources.

Suggested Fix

Add a scheme check in NavigationURLLoaderImpl::OnReceiveEarlyHints to ignore early hints if the navigation is for a blob: URL. Early hints should only be accepted for standard http: or https: network schemes.

void NavigationURLLoaderImpl::OnReceiveEarlyHints(
    network::mojom::EarlyHintsPtr early_hints) {
  DCHECK(!received_response_);
  DCHECK_NE(early_hints->ip_address_space,
            network::mojom::IPAddressSpace::kUnknown);

  // Loading a blob URL (or other non-network scheme) never produces early hints.
  if (request_info_->common_params->url.SchemeIsBlob()) {
    return;
  }
  ...

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