Medium watch CVSS 3.1 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper resource exposure in Preload
DescriptionImproper resource exposure in Preload
ComponentPreload
Sub-componentPredictors
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker503472696
Fix commit404123841316 (chromium/src) +200/-2
CWECWE-668 (Exposure to wrong sphere)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-16 (per vendor advisory)
Fix landed2026-07-02 (public git)
Disclosed2026-08-25
Patch gap54 days — fix public in source before the advisory

Background

`NetworkHintsHandlerImpl`
This is the browser-side Mojo implementation of network_hints::mojom::NetworkHintsHandler. It receives renderer-initiated PrefetchDNS and Preconnect hints, for example from <link rel=dns-prefetch> or <link rel=preconnect>, and forwards them to content::PreconnectManager.
`StoragePartition` / `StoragePartitionConfig`
A StoragePartition is an isolated bundle of cookies, cache and storage with its own NetworkContext. Guest contents such as <webview> and other non-default-partition frames run in their own partition, which is often in-memory.
`storage_partition_config` argument
PreconnectManager::StartPreresolveHosts and StartPreconnectUrl accept a const StoragePartitionConfig*. According to the commit message, passing nullptr makes the manager fall back to the profile’s default partition’s NetworkContext.

Root Cause Analysis

NetworkHintsHandlerImpl forwarded DNS-prefetch and preconnect hints from any frame with /*storage_partition_config=*/nullptr. PreconnectManager therefore issued the resulting DNS resolutions and socket preconnects through the profile’s default StoragePartition NetworkContext, whichever partition the requesting frame actually lived in. This broke the invariant that network activity started by a frame stays inside that frame’s partition. A guest or other non-default-partition frame could make the default partition do network work, possibly with allow_credentials set, and leave state behind in the default partition’s DNS cache and socket pools (inferred).

The fix looks up render_frame_host->GetStoragePartition()->GetConfig() and passes a pointer to that config in both calls, so the hint runs against the frame’s own NetworkContext. The new unit tests check that Pointee(kGuestConfig) reaches StartPreconnectUrl/StartPreresolveHosts for a guest frame and that the default config is used for a normal frame.

Key insight
The browser trusted a “null means default” fallback for renderer-initiated network hints instead of using the frame’s own storage partition, so isolated partitions leaked network activity into the default one. The fix always passes the frame’s StoragePartitionConfig explicitly.

Attack Path

  1. Content runs in a non-default partition Attacker-controlled web content is loaded in a frame whose StoragePartition is not the default, such as a <webview> guest created by an app or extension, as in the test’s SiteInstance::CreateForGuest. Ordinary tabs use the default partition and are unaffected.
  2. Issue network hints The page emits DNS-prefetch or preconnect hints (inferred: through standard resource-hint markup), which reach NetworkHintsHandlerImpl::PrefetchDNS or Preconnect over Mojo.
  3. Hints cross the partition boundary Before the patch, the handler passed nullptr as the config, so PreconnectManager resolved hosts and opened connections, credentialed if allow_credentials was set, in the default profile partition’s NetworkContext.
  4. Observe or influence default-partition state Warmed DNS entries or sockets in the default partition could then be visible to or affect other contexts, for example through timing, or the hints could bypass partition-specific network configuration (inferred).

Impact Assessment

The advisory classifies this as improper resource exposure in Preload and gives no exploitation status here. Content in a guest or other non-default StoragePartition could have the browser perform DNS and preconnect activity in the profile’s default partition, which weakens partition isolation. Exploitation requires an embedding scenario that uses a non-default partition, such as <webview>, and any resulting cross-partition tracking or information leak is (inferred).

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
TEST_F
chrome/browser/predictors/network_hints_handler_impl_unittest.cc
modified

Files Changed

  • chrome/browser/predictors/loading_predictor.h
  • chrome/browser/predictors/network_hints_handler_impl.cc
  • chrome/browser/predictors/BUILD.gn
  • chrome/browser/predictors/network_hints_handler_impl_unittest.cc

Audit Directions

  • Null-means-default partition parameters
    Find other callers of PreconnectManager, GetDefaultStoragePartition() or APIs that take an optional StoragePartitionConfig*/NetworkContext and pass nullptr for frame-initiated work. Prefetch, prerender, speculation rules and LoadingPredictor hints are good candidates.
  • Guest and isolated partitions
    Audit browser-side handlers bound per RenderFrameHost (network hints, the Reporting API, NEL, connection warming) to confirm they derive the NetworkContext from render_frame_host->GetStoragePartition() rather than from the Profile.
  • Cross-partition shared caches
    Check whether host cache and socket pool state, together with NetworkAnonymizationKey, can be warmed from one partition and observed from another when the key matches but the NetworkContext differs.
From 40412384131694389168cb37ae246b385abcfbf9 Mon Sep 17 00:00:00 2001
From: Jiacheng Guo <[email protected]>
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 <[email protected]>
Reviewed-by: Lingqi Chi <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1655924}
---
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));
+  }
+
+  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
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",
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 [email protected]

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