Low watch CVSS 4.8 chrome Authorization Error 🔧 Commit mapped

Overview

Low
Severity
4.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Certificate
DescriptionIncorrect authorization in Certificate
ComponentCertificate
Bug ClassAuthorization 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.
Tracker517776674
Fix commit925f969b4488 (chromium/src) +1493/-300
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-05-29 (per vendor advisory)
Fix landed2026-08-06 (public git)
Disclosed2026-09-08
Patch gap33 days — fix public in source before the advisory

Background

Multi-network CCT
Chrome Custom Tabs feature (Android) where a bound WebContents must send its network requests over a specific target network (stored in WebContents::GetTargetNetwork), e.g. captive-portal login.
`URLLoaderFactoryBuilder` / `WillCreateURLLoaderFactory`
the builder that assembles the chain of URLLoaderFactory objects for a request context, and the ContentBrowserClient hook where the browser can inject interceptors/overrides before the factory is finalized.
`MaybeProxyNetworkBoundRequest` (old) / `MaybeSetTargetNetwork` (new)
the ChromeContentBrowserClient method that binds requests to bound_network; the old form created a separate network_bound_network_context_, the new form sets target_network on the builder.
`is_for_network_service` / `RequiresFreshFactory`
a new parameter telling the client whether the factory being built is actually backed by the network service, and a new builder query for whether a fresh factory (rather than a shared one) must be created to honor the target network.

Root Cause Analysis

To bind CCT requests to a target network, ChromeContentBrowserClient::MaybeProxyNetworkBoundRequest created and cached a separate network_bound_network_context_ and spliced a proxying URLLoaderFactory (via factory_builder.Append() or a URLLoaderFactoryOverride) into whatever factory chain WillCreateURLLoaderFactory was assembling. Two problems follow from the diff. First, that separate NetworkContext was configured with a freshly minted, default cert_verifier_params — content::GetCertVerifierParams(cert_verifier::mojom::CertVerifierCreationParams::New()) — rather than the profile’s configured certificate-verification parameters, so certificate authorization for network-bound requests did not match the profile’s real policy. Second, the proxy was installed unconditionally, with no signal for whether the factory being built was even backed by the network service; target_network can only be honored by the network service, so applying it to non-network-service factories was meaningless or incorrect.

The fix replaces the separate context with factory_builder.SetTargetNetwork(bound_network), so the standard network-service-backed factory (using the storage partition’s proper NetworkContext and its correct cert verification) binds the network on demand. It gates this on a new is_for_network_service parameter threaded through WillCreateURLLoaderFactory, and uses RequiresFreshFactory() in the navigation and download loaders to build a fresh factory (instead of the browser’s shared one) when a target network is required.

Key insight
Network binding was implemented by grafting a bespoke NetworkContext with default certificate-verification parameters into the factory chain regardless of whether that chain was network-service-backed; the fix routes binding through the standard network-service factory via SetTargetNetwork, restoring the profile’s certificate verification and gating on is_for_network_service.

Attack Path

  1. Launch a network-bound Custom Tab An app starts a multi-network CCT that supplies a target network (as exercised by testNetworkBoundCustomTabIntent, which puts a Network in CustomTabsIntent.EXTRA_NETWORK), so the bound WebContents has a non-invalid bound_network.
  2. Requests routed through the separate context Under the old code, MaybeProxyNetworkBoundRequest builds/caches network_bound_network_context_ and proxies the request through it, and that context was created with default cert_verifier_params rather than the profile’s configured certificate-verification parameters.
  3. Certificate authorization mismatch Because the network-bound context does not carry the profile’s real cert-verifier configuration, certificate verification for the bound request applies incorrect authorization state relative to a normal request in the same profile (the “incorrect authorization in Certificate” the advisory describes).
  4. Broadened misapplication (inferred) Without an is_for_network_service signal, the binding path could also be reached for factory chains not backed by the network service, applying network-bound handling where it does not belong; the fix’s is_for_network_service gate removes this case.

Impact Assessment

The refactor removes a separate, network-bound NetworkContext that used default certificate-verification parameters instead of the profile’s configured ones, correcting certificate authorization for multi-network CCT requests and confining network binding to network-service-backed factories. The context is the browser process on Android, and the precondition is a multi-network Custom Tab bound to a target network. The advisory classifies this as incorrect authorization in Certificate; it reports no in-the-wild exploitation, and any concrete downstream security consequence of the mismatched verification (e.g., accepting a certificate that policy would reject) is (inferred) beyond what the diff shows.

Changed Functions

FunctionChangeNotes
if
chrome/browser/chrome_content_browser_client.cc
modified
get_network_bound_network_context_for_testing
chrome/browser/chrome_content_browser_client.h
modified
get_target_network_for_network_bound_network_context_for_testing
chrome/browser/chrome_content_browser_client.h
modified

Files Changed

  • chrome/browser/multi_network_browser_test.cc
  • content/browser/service_worker/service_worker_client.cc
  • services/network/network_context_unittest.cc
  • content/browser/preloading/prefetch/prefetch_url_loader_interceptor_unittest.cc
  • net/url_request/url_request_context.h
  • content/browser/loader/url_loader_factory_utils.h
  • chrome/browser/chrome_content_browser_client.h
  • android_webview/browser/aw_content_browser_client.h
  • content/browser/service_worker/service_worker_context_wrapper.cc
  • content/public/browser/content_browser_client.cc
  • content/browser/preloading/prefetch/nav_prefetch_browsertest.cc
  • services/network/test/test_network_context.h
  • services/network/public/cpp/url_loader_factory_builder_unittest.cc
  • chrome/browser/OWNERS
  • content/browser/loader/url_loader_factory_utils.cc
  • content/browser/download/download_manager_impl.cc
  • services/network/public/cpp/url_loader_factory_builder.cc
  • chrome/browser/chrome_content_browser_client.cc
  • content/test/url_loader_interceptor_browsertest.cc
  • chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java
  • chrome/browser/android/content/java/src/org/chromium/chrome/browser/content/WebContentsFactory.java
  • net/url_request/url_request_context.cc
  • content/browser/preloading/prefetch/prefetch_test_util_internal.h
  • services/network/network_context.h
  • android_webview/browser/aw_content_browser_client.cc
  • content/browser/preloading/prefetch/prefetch_network_context_unittest.cc
  • content/browser/web_contents/web_contents_impl.h
  • services/network/public/cpp/url_loader_factory_builder.h
  • content/browser/preloading/prefetch/prefetch_test_util_internal.cc
  • content/browser/loader/navigation_url_loader_impl.cc
  • services/network/url_loader.cc
  • services/network/public/mojom/network_context.mojom
  • content/public/browser/content_browser_client.h
  • chrome/test/BUILD.gn
  • services/network/url_loader_factory.cc
  • chrome/browser/chrome_content_browser_client_browsertest.cc
  • services/network/network_context.cc

Audit Directions

  • Ad-hoc `NetworkContext` creation with default security params
    Look for code that spins up its own NetworkContext/NetworkContextParams and passes freshly constructed CertVerifierCreationParams (or other verification/policy params) rather than deriving them from the profile via ConfigureNetworkContextParams.
  • Interceptors applied without a network-service backing check
    Audit WillCreateURLLoaderFactory implementations and factory-chain interceptors that assume the network service backs the factory; confirm they honor the is_for_network_service signal before applying network-only behavior.
  • Renderer/embedder-supplied network targeting
    Review paths that bind requests to an externally supplied network handle (CCT EXTRA_NETWORK, WebContents::GetTargetNetwork) to ensure the binding uses the correctly configured factory and cannot downgrade certificate verification.
From 925f969b44885626d960df90670845f2d7a3963e Mon Sep 17 00:00:00 2001
From: Stefano Duo <stefanoduo@google.com>
Date: Thu, 06 Aug 2026 11:02:35 -0700
Subject: [PATCH] Refactor multi-network CCT to rely on net::UrlRequest's target_network

In multi-network Chrome Custom Tabs (CCT), network requests originating
from a bound WebContents must be bound to a specific target network
(stored in WebContents::GetTargetNetwork). Previously, this binding was
accomplished by creating and maintaining a separate, network-bound
NetworkContext (network_bound_network_context_) inside
ChromeContentBrowserClient, then routing requests to this separate
network-context. This approach was complex and memory-intensive.

This CL refactors the target network binding mechanism by plumbing a
target_network parameter directly into
network.mojom.URLLoaderFactoryParams, built on top of the new
target_network parameter needed to create a net::URLRequest. Under this
architecture, standard network factories can bind requests to a
specified network directly on demand, without requiring a separate
NetworkContext.

The plumbing between WebContents' target_network, and
network.mojom.URLLoaderFactoryParams's new target_network parameter is
still implemented via WillCreateURLLoaderFactory, but it now relies on
two new URLLoaderFactoryBuilder methods:
1. SetTargetNetwork - stores the target_network and later uses it when
   building the final URLLoaderFactory
2. RequiresFreshFactory - used to figure out whether a pre-existing
   shared URLLoaderFactory can be used, or a fresh one has to be
   created. This is necessary to target a network for navigations (and
   downloads). Navigations usually use the browser process shared
   URLLoaderFactory. This is not possible for multi-network CCT
   because it requires an URLLoaderFactory that target a network and
   the browser shared URLLoaderFactory does not. In this scenario we
   instead create a new URLLoaderFactory from scratch

target_network can only be consumed by the network service. With that in
mind, SetTargetNetwork should only be called when
URLLoaderFactoryBuilder will build a fresh URLLoaderFactory within
ConnectTerminal. This is enforced by:
1. Calling SetTargetNetwork only when we know the network service is
   going to back the URLLoaderFactory that will be created.
2. Explicitly crashing in the ConnectTerminal variants that do not
   connect to a URLLoaderFactory built from scratch.
3. Adding a new is_for_network_service parameter to
   WillCreateURLLoaderFactory

Bug: 449098586, 488878730, 517776674
Change-Id: I7a2c7da0152f853c479fb0c9a2d6f5ea18abb613
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7904261
Reviewed-by: Adam Rice <ricea@chromium.org>
Commit-Queue: Stefano Duo <stefanoduo@google.com>
Reviewed-by: Victor Vianna <victorvianna@google.com>
Auto-Submit: Stefano Duo <stefanoduo@google.com>
Cr-Commit-Position: refs/heads/main@{#1675111}
---
diff --git a/android_webview/browser/aw_content_browser_client.cc b/android_webview/browser/aw_content_browser_client.cc
index 7ea6cee..80d295d 100644
--- a/android_webview/browser/aw_content_browser_client.cc
+++ b/android_webview/browser/aw_content_browser_client.cc
@@ -1199,7 +1199,8 @@
     bool* bypass_redirect_checks,
     bool* disable_secure_dns,
     network::mojom::URLLoaderFactoryOverridePtr* factory_override,
-    scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner) {
+    scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner,
+    bool is_for_network_service) {
   TRACE_EVENT0("android_webview",
                "AwContentBrowserClient::WillCreateURLLoaderFactory");
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
diff --git a/android_webview/browser/aw_content_browser_client.h b/android_webview/browser/aw_content_browser_client.h
index a2adea2..84c29a2 100644
--- a/android_webview/browser/aw_content_browser_client.h
+++ b/android_webview/browser/aw_content_browser_client.h
@@ -247,8 +247,8 @@
       bool* bypass_redirect_checks,
       bool* disable_secure_dns,
       network::mojom::URLLoaderFactoryOverridePtr* factory_override,
-      scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner)
-      override;
+      scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner,
+      bool is_for_network_service) override;
   content::ContentBrowserClient::WebSocketOptions GetWebSocketOptions(
       content::RenderFrameHost* frame) override;
   bool WillCreateRestrictedCookieManager(
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java
index 31a89d5..bc516cbb 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java
@@ -901,6 +901,39 @@
 
     @Test
     @SmallTest
+    @MinAndroidSdkLevel(Build.VERSION_CODES.M)
+    public void testNetworkBoundCustomTabIntent() throws Exception {
+        CustomTabsConnection realConnection = CustomTabsConnection.getInstance();
+        CustomTabsConnection mockConnection = Mockito.spy(realConnection);
+        CustomTabsConnection.setInstanceForTesting(mockConnection);
+
+        // This Network object has to be sent via an Intent extra. With that in mind, it's much
+        // easier to create a "real" Network object, instead of mocking it. This requires a bit of
+        // "magic".
+        long fakeNetId = 99999;
+        long magic = 0xcafed00dL;
+        long fakeNetworkHandle = (fakeNetId << 32) | magic;
+        android.net.Network network = android.net.Network.fromNetworkHandle(fakeNetworkHandle);
+        doReturn(network).when(mockConnection).extractTargetNetwork(any(), any());
+
+        Intent intent =
+                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(
+                        ApplicationProvider.getApplicationContext(), mTestPage);
+        intent.putExtra(CustomTabsIntent.EXTRA_NETWORK, network);
+
+        // We need a session to make it valid.
+        var token = SessionHolder.getSessionHolderFromIntent(intent);
+        realConnection.newSession(token.getSessionAsCustomTab());
+
+        // Launch. It should attempt to load mTestPage but fail due to invalid network.
+        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
+
+        Tab tab = getActivity().getActivityTab();
+        assertTrue(tab.isShowingErrorPage());
+    }
+
+    @Test
+    @SmallTest
     public void testRecordRetainableSession_WithCctSession() throws Exception {
         Activity emptyActivity = startBlankUiTestActivity();
 
diff --git a/chrome/browser/android/content/java/src/org/chromium/chrome/browser/content/WebContentsFactory.java b/chrome/browser/android/content/java/src/org/chromium/chrome/browser/content/WebContentsFactory.java
index 7b6dbad..3ae92cf8 100644
--- a/chrome/browser/android/content/java/src/org/chromium/chrome/browser/content/WebContentsFactory.java
+++ b/chrome/browser/android/content/java/src/org/chromium/chrome/browser/content/WebContentsFactory.java
@@ -62,8 +62,8 @@
      * @param initiallyHidden Whether or not the {@link WebContents} should be initially hidden.
      * @param initializeRenderer Whether or not the {@link WebContents} should initialize renderer.
      * @param targetNetwork target bound network, also refer to the documentation of {@link
-     *     ChromeContentBrowserClient::MaybeProxyNetworkBoundRequest} on how to use targetNetwork at
-     *     the native layer.
+     *     ChromeContentBrowserClient::MaybeSetTargetNetwork} on how to use targetNetwork at the
+     *     native layer.
      * @return A newly created {@link WebContents} object.
      */
     public static WebContents createWebContents(
diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc
index 5324106..97d767c 100644
--- a/chrome/browser/chrome_content_browser_client.cc
+++ b/chrome/browser/chrome_content_browser_client.cc
@@ -1729,74 +1729,15 @@
   g_disable_advanced_protection_caching_for_tests = true;
 }
 
-void ChromeContentBrowserClient::MaybeProxyNetworkBoundRequest(
-    content::BrowserContext* browser_context,
+void ChromeContentBrowserClient::MaybeSetTargetNetwork(
     net::handles::NetworkHandle bound_network,
     network::URLLoaderFactoryBuilder& factory_builder,
-    network::mojom::URLLoaderFactoryOverridePtr* factory_override,
-    const net::IsolationInfo& isolation_info) {
-  if (bound_network == net::handles::kInvalidNetworkHandle) {
+    bool is_for_network_service) {
+  if (bound_network == net::handles::kInvalidNetworkHandle ||
+      !is_for_network_service) {
     return;
   }
-
-  // We support one network-bound NetworkContext at most. If a new one is
-  // needed, make sure to clean up the previous one first.
-  if (bound_network != target_network_for_network_bound_network_context_) {
-    network_bound_network_context_ =
-        mojo::Remote<network::mojom::NetworkContext>();
-    network::mojom::NetworkContextParamsPtr context_params =
-        network::mojom::NetworkContextParams::New();
-    context_params->bound_network = bound_network;
-    context_params->cert_verifier_params = content::GetCertVerifierParams(
-        cert_verifier::mojom::CertVerifierCreationParams::New());
-    context_params->enable_domain_reliability = false;
-    ConfigureNetworkContextParams(
-        browser_context, true, base::FilePath(), context_params.get(),
-        cert_verifier::mojom::CertVerifierCreationParams::New().get());
-    content::CreateNetworkContextInNetworkService(
-        network_bound_network_context_.BindNewPipeAndPassReceiver(),
-        std::move(context_params));
-    target_network_for_network_bound_network_context_ = bound_network;
-  }
-
-  // TLDR; if `factory_override` != nullptr, this is being called for the
-  // creation of a 2-layer URLLoaderFactory (see
-  // network.mojom.URLLoaderFactoryOverride documentation). In this case, we
-  // want to substitute the internal (defined by
-  // factory_override->overriding_factory, with a URLLoaderFactory that targets
-  // `bound_network`. If `factory_override` == nullptr, this is a single-layer
-  // URLLoaderFactory. In this case, we want the last URLLoaderFactory in the
-  // `factory_builder` chain to be a URLLoaderFactory that targets
-  // `bound_network`.
-  mojo::PendingReceiver<network::mojom::URLLoaderFactory> proxied_receiver;
-  mojo::PendingRemote<network::mojom::URLLoaderFactory> bypassed_remote;
-  if (!factory_override) {
-    // Hijack the receiver end returned by network::URLLoaderFactoryBuilder.
-    // This will be then redirected to a network-bound URLLoaderFactory.
-    std::tie(proxied_receiver, bypassed_remote) = factory_builder.Append();
-  } else {
-    // Hijack the remote end stored in network::mojom::URLLoaderFactoryOverride.
-    // This will be then redirected to a network-bound URLLoaderFactory.
-    *factory_override = network::mojom::URLLoaderFactoryOverride::New();
-    proxied_receiver =
-        (*factory_override)
-            ->overriding_factory.InitWithNewPipeAndPassReceiver();
-    (*factory_override)->overridden_factory_receiver =
-        bypassed_remote.InitWithNewPipeAndPassReceiver();
-    (*factory_override)->skip_cors_enabled_scheme_check = true;
-  }
-
-  // Create a network-bound URLLoaderFactory and redirect the receiver end of
-  // the hijacked remote to this.
-  network::mojom::URLLoaderFactoryParamsPtr params =
-      network::mojom::URLLoaderFactoryParams::New();
-  params->process_id = network::OriginatingProcessId::browser();
-  params->is_trusted = true;
-  params->isolation_info = isolation_info;
-  // Disable CORS wrapping, this is already handled by the caller.
-  params->disable_web_security = true;
-  network_bound_network_context_->CreateURLLoaderFactory(
-      std::move(proxied_receiver), std::move(params));
+  factory_builder.SetTargetNetwork(bound_network);
 }
 
 std::unique_ptr<content::BrowserMainParts>
@@ -6666,7 +6607,8 @@
     bool* bypass_redirect_checks,
     bool* disable_secure_dns,
     network::mojom::URLLoaderFactoryOverridePtr* factory_override,
-    scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner) {
+    scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner,
+    bool is_for_network_service) {
 #if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
   auto* web_request_api =
       extensions::BrowserContextKeyedAPIFactory<extensions::WebRequestAPI>::Get(
@@ -6729,12 +6671,8 @@
 #endif  // BUILDFLAG (ENABLE_EXTENSIONS_CORE)
 #endif  // BUILDFLAG(ENABLE_GUEST_VIEW)
 
-  // WARNING: This must be the last interceptor in the chain as the proxying
-  // URLLoaderFactory installed by this needs to be the one actually sending
-  // packets over the network (to effectively target `bound_network`).
-  MaybeProxyNetworkBoundRequest(
-      browser_context, GetBoundNetworkFromRenderFrameHost(frame),
-      factory_builder, factory_override, isolation_info);
+  MaybeSetTargetNetwork(GetBoundNetworkFromRenderFrameHost(frame),
+                        factory_builder, is_for_network_service);
 
 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || \
     BUILDFLAG(IS_CHROMEOS)
diff --git a/chrome/browser/chrome_content_browser_client.h b/chrome/browser/chrome_content_browser_client.h
index f37d9b89..8b14900 100644
--- a/chrome/browser/chrome_content_browser_client.h
+++ b/chrome/browser/chrome_content_browser_client.h
@@ -702,8 +702,8 @@
       bool* bypass_redirect_checks,
       bool* disable_secure_dns,
       network::mojom::URLLoaderFactoryOverridePtr* factory_override,
-      scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner)
-      override;
+      scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner,
+      bool is_for_network_service) override;
   std::vector<std::unique_ptr<content::URLLoaderRequestInterceptor>>
   WillCreateURLLoaderRequestInterceptors(
       content::NavigationUIData* navigation_ui_data,
@@ -1346,29 +1346,15 @@
       std::unique_ptr<ScopedKeepAlive> keep_alive_handle);
 #endif
 
-  // If `bound_network` != net::base::kInvalidNetworkHandle, this will make sure
-  // tha the chain of URLLoaderFactories will end up with a network factory that
-  // knows how to target `bound_network` (see
-  // network.mojom.NetworkContextParams::bound_network documentation).
-  // WARNING: This must be the last interceptor in the chain as the proxying
-  // URLLoaderFactory installed by this needs to be the one actually sending
-  // packets over the network (to effectively target `bound_network`).
-  void MaybeProxyNetworkBoundRequest(
-      content::BrowserContext* browser_context,
-      net::handles::NetworkHandle bound_network,
-      network::URLLoaderFactoryBuilder& factory_builder,
-      network::mojom::URLLoaderFactoryOverridePtr* factory_override,
-      const net::IsolationInfo& isolation_info);
-
-  mojo::Remote<network::mojom::NetworkContext>&
-  get_network_bound_network_context_for_testing() {
-    return network_bound_network_context_;
-  }
-
-  net::handles::NetworkHandle
-  get_target_network_for_network_bound_network_context_for_testing() const {
-    return target_network_for_network_bound_network_context_;
-  }
+  // If `factory_builder` will be backed by the network service
+  // (`is_for_network_service` == true), and
+  // `bound_network` != net::handles::kInvalidNetworkHandle, this makes sure
+  // that the chain of URLLoaderFactories will end up with a network factory
+  // that knows how to target `bound_network` (see
+  // network.mojom.URLLoaderFactoryParams::target_network).
+  void MaybeSetTargetNetwork(net::handles::NetworkHandle bound_network,
+                             network::URLLoaderFactoryBuilder& factory_builder,
+                             bool is_for_network_service);
 
   // True if the Gaia origin should be isolated in a dedicated process.
   static bool DoesGaiaOriginRequireDedicatedProcess();
@@ -1421,28 +1407,6 @@
   std::string GetChildProcessSuffix(int child_flags) override;
 #endif  // BUILDFLAG(IS_MAC)
 
-  // NetworkContext used by WebContents targeting a network. Currently, due to
-  // the intended use-case (captive portal login over CCT), we support only a
-  // single additional NetworkContext: this simplifies the lifetime handling by
-  // *a lot*.
-  // Whenever a WebContent targeting a new network issues a load request, the
-  // previous NetworkContext will be destroyed and a new one will be created.
-  // This can lead to "trashing" when multiple WebContents, targeting different
-  // networks, are loading resources simultaneuously, as they will keep
-  // destroying each other's NetworkContext. This is a known limitation of the
-  // current design, but, as mentioned above, this should not happened in the
-  // intended use-case (there is no expectation that a user will connect to
-  // multiple captive portals at once).
-  mojo::Remote<network::mojom::NetworkContext> network_bound_network_context_;
-
-  // Network to which `network_bound_network_context_` is bound to. If ==
-  // net::handles::kInvalidNetworkHandle, `network_bound_network_context_` is
-  // currently in a pending remote state (i.e., no underlying NetworkContext
-  // exists).
-  net::handles::NetworkHandle
-      target_network_for_network_bound_network_context_ =
-          net::handles::kInvalidNetworkHandle;
-
   // Tracks whether the browser was started in "minimal" mode (as opposed to
   // full browser mode), where most subsystems are not initialized.
   bool is_minimal_mode_ = false;
diff --git a/content/browser/download/download_manager_impl.cc b/content/browser/download/download_manager_impl.cc
index 63ded353..0fbabce 100644
--- a/content/browser/download/download_manager_impl.cc
+++ b/content/browser/download/download_manager_impl.cc
@@ -417,7 +417,19 @@
         ukm::kInvalidSourceIdObj, factory_builder, /*header_client=*/nullptr,
         /*bypass_redirect_checks=*/nullptr, /*disable_secure_dns=*/nullptr,
         /*factory_override=*/nullptr,
-        /*navigation_response_task_runner=*/nullptr);
+        /*navigation_response_task_runner=*/nullptr,
+        /*is_for_network_service=*/true);
+  }
+
+  if (factory_builder.RequiresFreshFactory()) {
+    network::mojom::URLLoaderFactoryParamsPtr params =
+        storage_partition->CreateURLLoaderFactoryParams();
+    auto factory =
+        std::move(factory_builder)
+            .Finish<mojo::PendingRemote<network::mojom::URLLoaderFactory>>(
+                storage_partition->GetNetworkContext(), std::move(params));
+    return std::make_unique<network::WrapperPendingSharedURLLoaderFactory>(
+        std::move(factory));
   }
 
   return std::make_unique<network::PendingSharedURLLoaderFactoryWithBuilder>(
diff --git a/content/browser/loader/navigation_url_loader_impl.cc b/content/browser/loader/navigation_url_loader_impl.cc
index bfd819c..80800b7 100644
--- a/content/browser/loader/navigation_url_loader_impl.cc
+++ b/content/browser/loader/navigation_url_loader_impl.cc
@@ -2265,7 +2265,8 @@
       frame_tree_node->navigation_request()->GetNavigationId(), ukm_id,
       factory_builder, &header_client, bypass_redirect_checks,
       /*disable_secure_dns=*/nullptr, /*factory_override=*/nullptr,
-      GetUIThreadTaskRunner({BrowserTaskType::kNavigationNetworkResponse}));
+      GetUIThreadTaskRunner({BrowserTaskType::kNavigationNetworkResponse}),
+      /*is_for_network_service=*/true);
 
   auto devtools_params =
       devtools_instrumentation::WillCreateURLLoaderFactoryParams::ForFrame(
@@ -2299,7 +2300,8 @@
             std::move(cookie_overrides),
             network::GetNoOpNetworkRestrictionsId()));
   } else {
-    if (!devtools_cookie_overrides.empty() || !cookie_overrides.empty()) {
+    if (!devtools_cookie_overrides.empty() || !cookie_overrides.empty() ||
+        factory_builder.RequiresFreshFactory()) {
       network::mojom::URLLoaderFactoryParamsPtr params =
           storage_partition->CreateURLLoaderFactoryParams();
       params->devtools_cookie_setting_overrides =
diff --git a/content/browser/loader/url_loader_factory_utils.cc b/content/browser/loader/url_loader_factory_utils.cc
index 11fc419..9a59803 100644
--- a/content/browser/loader/url_loader_factory_utils.cc
+++ b/content/browser/loader/url_loader_factory_utils.cc
@@ -58,7 +58,8 @@
     DisableSecureDnsOption disable_secure_dns_option,
     StoragePartitionImpl* storage_partition,
     std::optional<URLLoaderFactoryTypes> url_loader_factory,
-    int process_id)
+    int process_id,
+    bool is_for_network_service)
     : network_context_(network_context),
       factory_params_(std::move(factory_params)),
       header_client_option_(header_client_option),
@@ -66,7 +67,8 @@
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/chrome_content_browser_client_browsertest.cc b/chrome/browser/chrome_content_browser_client_browsertest.cc
index 3442710..973f934 100644
--- a/chrome/browser/chrome_content_browser_client_browsertest.cc
+++ b/chrome/browser/chrome_content_browser_client_browsertest.cc
@@ -1890,95 +1890,6 @@
 };
 
 IN_PROC_BROWSER_TEST_F(TopChromeChromeContentBrowserClientTest,
-                       UnboundRequestDoesNothing) {
-#if BUILDFLAG(IS_ANDROID)
-  network::URLLoaderFactoryBuilder factory_builder;
-  client()->MaybeProxyNetworkBoundRequest(browser()->GetProfile(),
-                                          net::handles::kInvalidNetworkHandle,
-                                          factory_builder, nullptr);
-  EXPECT_EQ(
-      client()
-          ->get_target_network_for_network_bound_network_context_for_testing(),
-      net::handles::kInvalidNetworkHandle);
-  EXPECT_FALSE(
-      client()->get_network_bound_network_context_for_testing().is_bound());
-#else   // !BUILDFLAG(IS_ANDROID)
-  GTEST_SKIP() << "proxying bound requests is supported only on Android";
-#endif  // BUILDFLAG(IS_ANDROID)
-}
-
-IN_PROC_BROWSER_TEST_F(TopChromeChromeContentBrowserClientTest,
-                       BoundRequestCreatesNetworkContext) {
-#if BUILDFLAG(IS_ANDROID)
-  constexpr net::handles::NetworkHandle network = 1;
-  network::URLLoaderFactoryBuilder factory_builder;
-  client()->MaybeProxyNetworkBoundRequest(browser()->GetProfile(), network,
-                                          factory_builder, nullptr);
-  EXPECT_EQ(
-      client()
-          ->get_target_network_for_network_bound_network_context_for_testing(),
-      network);
-  EXPECT_TRUE(
-      client()->get_network_bound_network_context_for_testing().is_bound());
-  EXPECT_TRUE(
-      client()->get_network_bound_network_context_for_testing().is_connected());
-  {
-    base::RunLoop run_loop;
-    client()
-        ->get_network_bound_network_context_for_testing()
-        ->GetBoundNetworkForTesting(base::BindOnce(
-            [](base::OnceClosure callback,
-               net::handles::NetworkHandle bound_network) {
-              EXPECT_EQ(bound_network, network);
-              std::move(callback).Run();
-            },
-            run_loop.QuitClosure()));
-    run_loop.Run();
-  }
-#else   // !BUILDFLAG(IS_ANDROID)
-  GTEST_SKIP() << "proxying bound requests is supported only on Android";
-#endif  // BUILDFLAG(IS_ANDROID)
-}
-
-IN_PROC_BROWSER_TEST_F(TopChromeChromeContentBrowserClientTest,
-                       BoundRequestWithOverrideCreatesNetworkContext) {
-#if BUILDFLAG(IS_ANDROID)
-  constexpr net::handles::NetworkHandle network = 1;
-  network::URLLoaderFactoryBuilder factory_builder;
-  network::mojom::URLLoaderFactoryOverridePtr factory_override;
-  EXPECT_FALSE(factory_override);
-  client()->MaybeProxyNetworkBoundRequest(browser()->GetProfile(), network,
-                                          factory_builder, &factory_override);
-  EXPECT_EQ(
-      client()
-          ->get_target_network_for_network_bound_network_context_for_testing(),
-      network);
-  EXPECT_TRUE(
-      client()->get_network_bound_network_context_for_testing().is_bound());
-  EXPECT_TRUE(
-      client()->get_network_bound_network_context_for_testing().is_connected());
-  EXPECT_TRUE(factory_override->overriding_factory);
-  mojo::Remote<network::mojom::URLLoaderFactory> overridden_factory;
-  overridden_factory.Bind(std::move(factory_override->overriding_factory));
-  {
-    base::RunLoop run_loop;
-    client()
-        ->get_network_bound_network_context_for_testing()
-        ->GetBoundNetworkForTesting(base::BindOnce(
-            [](base::OnceClosure callback,
-               net::handles::NetworkHandle bound_network) {
-              EXPECT_EQ(bound_network, network);
-              std::move(callback).Run();
-            },
-            run_loop.QuitClosure()));
-    run_loop.Run();
-  }
-#else   // !BUILDFLAG(IS_ANDROID)
-  GTEST_SKIP() << "proxying bound requests is supported only on Android";
-#endif  // BUILDFLAG(IS_ANDROID)
-}
-
-IN_PROC_BROWSER_TEST_F(TopChromeChromeContentBrowserClientTest,
                        ShouldReuseRendererWhenTopChromePagesPresent) {
   const GURL top_chrome_url(chrome::kChromeUITabSearchURL);
   const GURL top_chrome_url2(chrome::kChromeUIReadLaterURL);
diff --git a/chrome/browser/multi_network_browser_test.cc b/chrome/browser/multi_network_browser_test.cc
new file mode 100644
index 0000000..5fa4ae4
--- /dev/null
+++ b/chrome/browser/multi_network_browser_test.cc
@@ -0,0 +1,1144 @@
+// 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 <algorithm>
+#include <map>
+#include <vector>
+
+#include "base/command_line.h"
+#include "base/functional/bind.h"
+#include "base/memory/raw_ptr.h"
+#include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
+#include "base/test/bind.h"
+#include "base/test/run_until.h"
+#include "base/test/scoped_feature_list.h"
+#include "build/build_config.h"
+#include "chrome/browser/chrome_content_browser_client.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/tab_list/tab_list_interface.h"
+#include "chrome/test/base/chrome_test_utils.h"
+#include "chrome/test/base/platform_browser_test.h"
+#include "components/download/public/common/download_url_parameters.h"
+#include "content/public/browser/download_manager.h"
+#include "content/public/browser/download_request_utils.h"
+#include "content/public/browser/navigation_controller.h"
+#include "content/public/browser/network_service_instance.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/render_process_host.h"
+#include "content/public/browser/service_worker_context.h"
+#include "content/public/browser/site_instance.h"
+#include "content/public/browser/storage_partition.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/browser/web_contents_delegate.h"
+#include "content/public/common/content_client.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "content/public/test/download_test_observer.h"
+#include "content/public/test/test_navigation_observer.h"
+#include "extensions/buildflags/buildflags.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "mojo/public/cpp/bindings/sync_call_restrictions.h"
+#include "net/base/isolation_info.h"
+#include "net/base/network_handle.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/http_response.h"
+#include "net/test/embedded_test_server/install_default_websocket_handlers.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "services/network/public/cpp/network_switches.h"
+#include "services/network/public/cpp/url_loader_factory_builder.h"
+#include "services/network/public/mojom/network_context.mojom.h"
+#include "services/network/public/mojom/network_service.mojom.h"
+#include "services/network/public/mojom/network_service_test.mojom.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/features.h"
+#include "third_party/blink/public/common/service_worker/service_worker_status_code.h"
+#include "third_party/blink/public/mojom/service_worker/service_worker_registration_options.mojom.h"
+
+namespace {
+
+using URLLoaderFactoryType =
+    content::ContentBrowserClient::URLLoaderFactoryType;
+
+#if !BUILDFLAG(ENABLE_EXTENSIONS_CORE)
+// URLLoaderFactories for filesystem: and data: schemes are not backed by the
+// network service. With that in mind, they should never be using the target
+// network.
+constexpr size_t kExpectedNonNetworkFactories = 2u;
+#else
+// On Desktop Android and Desktop platforms, when extensions are enabled,
+// URLLoaderFactories for chrome-extension: scheme are also not backed by the
+// network service, this means they will also not be using the target network.
+constexpr size_t kExpectedNonNetworkFactories = 3u;
+#endif
+
+class TestChromeContentBrowserClient : public ChromeContentBrowserClient {
+ public:
+  TestChromeContentBrowserClient()
+      : old_client_(content::SetBrowserClientForTesting(this)) {}
+  ~TestChromeContentBrowserClient() override {
+    content::SetBrowserClientForTesting(old_client_);
+  }
+
+  void WillCreateURLLoaderFactory(
+      content::BrowserContext* browser_context,
+      content::RenderFrameHost* frame,
+      int render_process_id,
+      URLLoaderFactoryType type,
+      const url::Origin& request_initiator,
+      const net::IsolationInfo& isolation_info,
+      std::optional<int64_t> navigation_id,
+      ukm::SourceIdObj ukm_source_id,
+      network::URLLoaderFactoryBuilder& factory_builder,
+      mojo::PendingRemote<network::mojom::TrustedURLLoaderHeaderClient>*
+          header_client,
+      bool* bypass_redirect_checks,
+      bool* disable_secure_dns,
+      network::mojom::URLLoaderFactoryOverridePtr* factory_override,
+      scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner,
+      bool is_for_network_service) override {
+    ChromeContentBrowserClient::WillCreateURLLoaderFactory(
+        browser_context, frame, render_process_id, type, request_initiator,
+        isolation_info, navigation_id, ukm_source_id, factory_builder,
+        header_client, bypass_redirect_checks, disable_secure_dns,
+        factory_override, navigation_response_task_runner,
+        is_for_network_service);
+    factory_bound_networks_[type].push_back(
+        factory_builder.target_network_for_testing());
+    factory_is_for_network_service_[type].push_back(is_for_network_service);
+  }
+
+  bool HasBoundNetwork(URLLoaderFactoryType type,
+                       net::handles::NetworkHandle network) const {
+    return CountBoundNetwork(type, network) > 0;
+  }
+
+  size_t CountBoundNetwork(URLLoaderFactoryType type,
+                           net::handles::NetworkHandle network) const {
+    auto it = factory_bound_networks_.find(type);
+    if (it != factory_bound_networks_.end()) {
+      return std::ranges::count(it->second, network);
+    }
+    return 0;
+  }
+
+  size_t CountBoundNetwork(net::handles::NetworkHandle network) const {
+    size_t count = 0;
+    for (const auto& [type, networks] : factory_bound_networks_) {
+      count += std::ranges::count(networks, network);
+    }
+    return count;
+  }
+
+  bool HasIsForNetworkService(URLLoaderFactoryType type, bool expected) const {
+    auto it = factory_is_for_network_service_.find(type);
+    if (it != factory_is_for_network_service_.end()) {
+      return std::ranges::contains(it->second, expected);
+    }
+    return false;
+  }
+
+ private:
+  raw_ptr<content::ContentBrowserClient> old_client_;
+  std::map<URLLoaderFactoryType, std::vector<net::handles::NetworkHandle>>
+      factory_bound_networks_;
+  std::map<URLLoaderFactoryType, std::vector<bool>>
+      factory_is_for_network_service_;
+};
+
+constexpr auto kAllURLLoaderFactoryTypes = std::to_array({
+    URLLoaderFactoryType::kNavigation,
+    URLLoaderFactoryType::kDownload,
+    URLLoaderFactoryType::kDocumentSubResource,
+    URLLoaderFactoryType::kWorkerMainResource,
+    URLLoaderFactoryType::kWorkerSubResource,
+    URLLoaderFactoryType::kServiceWorkerScript,
+    URLLoaderFactoryType::kServiceWorkerSubResource,
+    URLLoaderFactoryType::kPrefetch,
+    URLLoaderFactoryType::kDevTools,
+    URLLoaderFactoryType::kEarlyHints,
+});
+
+struct ExpectedFactoryCounts {
+  size_t navigation = 0;
+  size_t download = 0;
+  size_t document_subresource = 0;
+  size_t worker_main_resource = 0;
+  size_t worker_subresource = 0;
+  size_t service_worker_script = 0;
+  size_t service_worker_subresource = 0;
+  size_t prefetch = 0;
+  size_t devtools = 0;
+  size_t early_hints = 0;
+  // The number of URLLoaderFactories that are not targeting a specific network
+  // is always expected to be the ones for schemes that are not supported by the
+  // network service. There is no need to specify this in each test.
+  size_t invalid_network_handle = kExpectedNonNetworkFactories;
+
+  size_t count_for(URLLoaderFactoryType type) const {
+    switch (type) {
+      case URLLoaderFactoryType::kNavigation:
+        return navigation;
+      case URLLoaderFactoryType::kDownload:
+        return download;
+      case URLLoaderFactoryType::kDocumentSubResource:
+        return document_subresource;
+      case URLLoaderFactoryType::kWorkerMainResource:
+        return worker_main_resource;
+      case URLLoaderFactoryType::kWorkerSubResource:
+        return worker_subresource;
+      case URLLoaderFactoryType::kServiceWorkerScript:
+        return service_worker_script;
+      case URLLoaderFactoryType::kServiceWorkerSubResource:
... (truncated)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.