Chrome · Certificate
CVE-2026-87463
Logic Error in Certificate
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/chrome_content_browser_client.cc |
modified |
Files Changed
android_webview/browser/aw_content_browser_client.ccandroid_webview/browser/aw_content_browser_client.hchrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.javachrome/browser/OWNERSchrome/browser/android/content/java/src/org/chromium/chrome/browser/content/WebContentsFactory.javachrome/browser/chrome_content_browser_client.cc
Patch
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/OWNERS b/chrome/browser/OWNERS
index 0d09651..d501030 100644
--- a/chrome/browser/OWNERS
+++ b/chrome/browser/OWNERS
@@ -61,6 +61,8 @@
per-file load_library_perf_test.*=xhwang@chromium.org
+per-file multi_network_browser_test.cc=stefanoduo@google.com
+
per-file *.mm=file://ui/base/cocoa/OWNERS
per-file *_mac.h=file://ui/base/cocoa/OWNERS
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;
- }
-
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.
References
On This Page