CVE-2026-79032
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
PrefetchLoadFlagsBrowserTestcontent/browser/loader/prefetch_browsertest.cc |
modified |
Files Changed
content/browser/loader/prefetch_browsertest.cc
Patch
From 477cd38a3be5889a17d121a58bfc4f18bfbc0cee Mon Sep 17 00:00:00 2001 From: Shunya Shishido <[email protected]> Date: Thu, 02 Jul 2026 19:32:28 -0700 Subject: [PATCH] [loader] Validate prefetch load_flags before trusted forwarding PrefetchURLLoaderServiceContext receives renderer-supplied ResourceRequests via SubresourceProxyingURLLoaderService. For cross-origin and recursive prefetches the request is forwarded to a CorsURLLoaderFactory created with is_trusted = true (via URLLoaderFactoryParamsHelper::CreateForPrefetch), which skips the load_flags allowlist that the network service applies to untrusted callers. This let the renderer supply flags such as LOAD_BYPASS_PROXY or LOAD_DISABLE_CERT_NETWORK_FETCHES on the forwarded request. Apply the same allowlist that CorsURLLoaderFactory uses for untrusted callers in CreatePrefetchLoaderAndStart, alongside the existing trusted_params check, and reject offending requests with ERR_INVALID_ARGUMENT and ReportBadMessage. Add browser tests that drive the SubresourceProxyingURLLoader factory directly and verify that restricted flags are rejected while renderer-permitted flags continue to be forwarded. TAG=agy CONV=fadb8099-bff9-487b-8731-17e10dc94af0 Bug: 498328139 Change-Id: I71f42166d3fd3db7726e7026cdcdbbbb61a39a71 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8030121 Reviewed-by: Kenichi Ishibashi <[email protected]> Reviewed-by: Takashi Toyoshima <[email protected]> Commit-Queue: Shunya Shishido <[email protected]> Cr-Commit-Position: refs/heads/main@{#1656322} --- diff --git a/content/browser/loader/prefetch_browsertest.cc b/content/browser/loader/prefetch_browsertest.cc index e1ba550..977527e 100644 --- a/content/browser/loader/prefetch_browsertest.cc +++ b/content/browser/loader/prefetch_browsertest.cc @@ -17,7 +17,11 @@ #include "base/threading/thread_restrictions.h" #include "build/build_config.h" #include "content/browser/loader/prefetch_browsertest_base.h" +#include "content/browser/loader/subresource_proxying_url_loader_service.h" +#include "content/browser/renderer_host/render_frame_host_impl.h" +#include "content/browser/storage_partition_impl.h" #include "content/browser/web_package/mock_signed_exchange_handler.h" +#include "content/browser/web_package/prefetched_signed_exchange_cache.h" #include "content/public/browser/browser_task_traits.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/web_contents.h" @@ -29,14 +33,20 @@ #include "content/public/test/test_frame_navigation_observer.h" #include "content/public/test/url_loader_monitor.h" #include "content/shell/browser/shell.h" +#include "mojo/public/cpp/test_support/test_utils.h" #include "net/base/features.h" #include "net/base/filename_util.h" #include "net/base/isolation_info.h" +#include "net/base/load_flags.h" #include "net/dns/mock_host_resolver.h" #include "net/test/embedded_test_server/default_handlers.h" #include "net/test/scoped_mutually_exclusive_feature_list.h" +#include "net/traffic_annotation/network_traffic_annotation_test_helper.h" #include "services/network/public/cpp/features.h" #include "services/network/public/cpp/resource_request.h" +#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" +#include "services/network/test/test_url_loader_client.h" +#include "services/network/test/test_url_loader_factory.h" #include "third_party/blink/public/common/features.h" namespace content { @@ -1133,6 +1143,150 @@ NavigateToURLAndWaitTitle(target_url, "Prefetch Target"); } +class PrefetchLoadFlagsBrowserTest : public PrefetchBrowserTestBase { + public: + PrefetchLoadFlagsBrowserTest() + : cross_origin_server_(std::make_unique<net::EmbeddedTestServer>()) {} + + void SetUpOnMainThread() override { + PrefetchBrowserTestBase::SetUpOnMainThread(); + host_resolver()->AddRule("*", "127.0.0.1"); + } + + protected: + // Binds a remote to the browser-side `SubresourceProxyingURLLoaderService` + // for the current main frame, mirroring the binding that the renderer would + // receive at navigation commit time. + mojo::Remote<network::mojom::URLLoaderFactory> BindFactoryForMainFrame() { + RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( + shell()->web_contents()->GetPrimaryMainFrame()); + StoragePartitionImpl* partition = + static_cast<StoragePartitionImpl*>(rfh->GetStoragePartition()); + mojo::Remote<network::mojom::URLLoaderFactory> remote; + partition->GetSubresourceProxyingURLLoaderService()->GetFactory( + remote.BindNewPipeAndPassReceiver(), rfh->GetFrameTreeNodeId(), + proxied_factory_.GetSafeWeakWrapper(), rfh->GetWeakPtr(), + /*prefetched_signed_exchange_cache=*/nullptr); + return remote; + } + + network::ResourceRequest CreateCrossOriginPrefetchRequest( + const GURL& target_url) { + network::ResourceRequest request; + request.url = target_url; + request.load_flags = + net::LOAD_PREFETCH | net::LOAD_RESTRICTED_PREFETCH_FOR_MAIN_FRAME; + request.request_initiator = shell() + ->web_contents() + ->GetPrimaryMainFrame() + ->GetLastCommittedOrigin(); + return request; + } + + std::unique_ptr<net::EmbeddedTestServer> cross_origin_server_; + network::TestURLLoaderFactory proxied_factory_; +}; + +// Verifies that a cross-origin prefetch request from the renderer is rejected +// if it carries load flags that are not permitted on requests originating from +// an untrusted process. The request reaches the network service via a trusted +// loader factory, so the browser must validate the flags before forwarding. +IN_PROC_BROWSER_TEST_F(PrefetchLoadFlagsBrowserTest, + CrossOriginPrefetchRejectsRestrictedLoadFlags) { + const char* prefetch_path = "/prefetch.html"; + const char* target_path = "/target.html"; + RegisterResponse(prefetch_path, ResponseEntry("<body></body>")); + RegisterResponse(target_path, + ResponseEntry("<head><title>Target</title></head>", + /*content_types=*/"")); + RegisterRequestHandler(embedded_test_server()); + RegisterRequestHandler(cross_origin_server_.get()); + ASSERT_TRUE(embedded_test_server()->Start()); + ASSERT_TRUE(cross_origin_server_->Start()); + + EXPECT_TRUE( + NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path))); + + const GURL target_url = + cross_origin_server_->GetURL("3p.example", target_path); + + mojo::Remote<network::mojom::URLLoaderFactory> factory = + BindFactoryForMainFrame(); + + for (int restricted_flag : + {net::LOAD_BYPASS_PROXY, net::LOAD_DISABLE_CERT_NETWORK_FETCHES}) { + SCOPED_TRACE(testing::Message() << "restricted_flag=" << restricted_flag); + + network::ResourceRequest request = + CreateCrossOriginPrefetchRequest(target_url); + request.load_flags |= restricted_flag; + + mojo::test::BadMessageObserver bad_message_observer; + network::TestURLLoaderClient client; + mojo::Remote<network::mojom::URLLoader> loader; + factory->CreateLoaderAndStart( + loader.BindNewPipeAndPassReceiver(), /*request_id=*/0, + network::mojom::kURLLoadOptionNone, request, client.CreateRemote(), + net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS)); + + client.RunUntilComplete(); + EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client.completion_status().error_code); + EXPECT_EQ("Prefetch/CreatePrefetchLoaderAndStart: restricted load flag", + bad_message_observer.WaitForBadMessage()); + + // The receiver is removed when a bad message is reported, so rebind for + // the next iteration. + factory.reset(); + factory = BindFactoryForMainFrame(); + } +} + +// Verifies that a cross-origin prefetch request carrying only load flags that +// are permitted on requests from an untrusted process is forwarded to the +// network service. +IN_PROC_BROWSER_TEST_F(PrefetchLoadFlagsBrowserTest, + CrossOriginPrefetchAllowsRendererLoadFlags) { + const char* prefetch_path = "/prefetch.html"; + const char* target_path = "/target.html"; + RegisterResponse(prefetch_path, ResponseEntry("<body></body>")); + RegisterResponse(target_path, + ResponseEntry("<head><title>Target</title></head>", + /*content_types=*/"")); + RegisterRequestHandler(embedded_test_server()); + + base::RunLoop prefetch_waiter; + auto request_counter = RequestCounter::CreateAndMonitor( + cross_origin_server_.get(), target_path, &prefetch_waiter); + RegisterRequestHandler(cross_origin_server_.get()); + ASSERT_TRUE(embedded_test_server()->Start()); + ASSERT_TRUE(cross_origin_server_->Start()); + + EXPECT_TRUE( + NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path))); + + const GURL target_url = + cross_origin_server_->GetURL("3p.example", target_path); +
Regression Test / PoC
diff --git a/content/browser/loader/prefetch_browsertest.cc b/content/browser/loader/prefetch_browsertest.cc
index e1ba550..977527e 100644
--- a/content/browser/loader/prefetch_browsertest.cc
+++ b/content/browser/loader/prefetch_browsertest.cc
@@ -17,7 +17,11 @@
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "content/browser/loader/prefetch_browsertest_base.h"
+#include "content/browser/loader/subresource_proxying_url_loader_service.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
+#include "content/browser/storage_partition_impl.h"
#include "content/browser/web_package/mock_signed_exchange_handler.h"
+#include "content/browser/web_package/prefetched_signed_exchange_cache.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
@@ -29,14 +33,20 @@
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/url_loader_monitor.h"
#include "content/shell/browser/shell.h"
+#include "mojo/public/cpp/test_support/test_utils.h"
#include "net/base/features.h"
#include "net/base/filename_util.h"
#include "net/base/isolation_info.h"
+#include "net/base/load_flags.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "net/test/scoped_mutually_exclusive_feature_list.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
+#include "services/network/test/test_url_loader_client.h"
+#include "services/network/test/test_url_loader_factory.h"
#include "third_party/blink/public/common/features.h"
namespace content {
@@ -1133,6 +1143,150 @@
NavigateToURLAndWaitTitle(target_url, "Prefetch Target");
}
+class PrefetchLoadFlagsBrowserTest : public PrefetchBrowserTestBase {
+ public:
+ PrefetchLoadFlagsBrowserTest()
+ : cross_origin_server_(std::make_unique<net::EmbeddedTestServer>()) {}
+
+ void SetUpOnMainThread() override {
+ PrefetchBrowserTestBase::SetUpOnMainThread();
+ host_resolver()->AddRule("*", "127.0.0.1");
+ }
+
+ protected:
+ // Binds a remote to the browser-side `SubresourceProxyingURLLoaderService`
+ // for the current main frame, mirroring the binding that the renderer would
+ // receive at navigation commit time.
+ mojo::Remote<network::mojom::URLLoaderFactory> BindFactoryForMainFrame() {
+ RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
+ shell()->web_contents()->GetPrimaryMainFrame());
+ StoragePartitionImpl* partition =
+ static_cast<StoragePartitionImpl*>(rfh->GetStoragePartition());
+ mojo::Remote<network::mojom::URLLoaderFactory> remote;
+ partition->GetSubresourceProxyingURLLoaderService()->GetFactory(
+ remote.BindNewPipeAndPassReceiver(), rfh->GetFrameTreeNodeId(),
+ proxied_factory_.GetSafeWeakWrapper(), rfh->GetWeakPtr(),
+ /*prefetched_signed_exchange_cache=*/nullptr);
+ return remote;
+ }
+
+ network::ResourceRequest CreateCrossOriginPrefetchRequest(
+ const GURL& target_url) {
+ network::ResourceRequest request;
+ request.url = target_url;
+ request.load_flags =
+ net::LOAD_PREFETCH | net::LOAD_RESTRICTED_PREFETCH_FOR_MAIN_FRAME;
+ request.request_initiator = shell()
+ ->web_contents()
+ ->GetPrimaryMainFrame()
+ ->GetLastCommittedOrigin();
+ return request;
+ }
+
+ std::unique_ptr<net::EmbeddedTestServer> cross_origin_server_;
+ network::TestURLLoaderFactory proxied_factory_;
+};
+
+// Verifies that a cross-origin prefetch request from the renderer is rejected
+// if it carries load flags that are not permitted on requests originating from
+// an untrusted process. The request reaches the network service via a trusted
+// loader factory, so the browser must validate the flags before forwarding.
+IN_PROC_BROWSER_TEST_F(PrefetchLoadFlagsBrowserTest,
+ CrossOriginPrefetchRejectsRestrictedLoadFlags) {
+ const char* prefetch_path = "/prefetch.html";
+ const char* target_path = "/target.html";
+ RegisterResponse(prefetch_path, ResponseEntry("<body></body>"));
+ RegisterResponse(target_path,
+ ResponseEntry("<head><title>Target</title></head>",
+ /*content_types=*/""));
+ RegisterRequestHandler(embedded_test_server());
+ RegisterRequestHandler(cross_origin_server_.get());
+ ASSERT_TRUE(embedded_test_server()->Start());
+ ASSERT_TRUE(cross_origin_server_->Start());
+
+ EXPECT_TRUE(
+ NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
+
+ const GURL target_url =
+ cross_origin_server_->GetURL("3p.example", target_path);
+
+ mojo::Remote<network::mojom::URLLoaderFactory> factory =
+ BindFactoryForMainFrame();
+
+ for (int restricted_flag :
+ {net::LOAD_BYPASS_PROXY, net::LOAD_DISABLE_CERT_NETWORK_FETCHES}) {
+ SCOPED_TRACE(testing::Message() << "restricted_flag=" << restricted_flag);
+
+ network::ResourceRequest request =
+ CreateCrossOriginPrefetchRequest(target_url);
+ request.load_flags |= restricted_flag;
+
+ mojo::test::BadMessageObserver bad_message_observer;
+ network::TestURLLoaderClient client;
+ mojo::Remote<network::mojom::URLLoader> loader;
+ factory->CreateLoaderAndStart(
+ loader.BindNewPipeAndPassReceiver(), /*request_id=*/0,
+ network::mojom::kURLLoadOptionNone, request, client.CreateRemote(),
+ net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+
+ client.RunUntilComplete();
+ EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client.completion_status().error_code);
+ EXPECT_EQ("Prefetch/CreatePrefetchLoaderAndStart: restricted load flag",
+ bad_message_observer.WaitForBadMessage());
+
+ // The receiver is removed when a bad message is reported, so rebind for
+ // the next iteration.
+ factory.reset();
+ factory = BindFactoryForMainFrame();
+ }
+}
+
+// Verifies that a cross-origin prefetch request carrying only load flags that
+// are permitted on requests from an untrusted process is forwarded to the
+// network service.
+IN_PROC_BROWSER_TEST_F(PrefetchLoadFlagsBrowserTest,
+ CrossOriginPrefetchAllowsRendererLoadFlags) {
+ const char* prefetch_path = "/prefetch.html";
+ const char* target_path = "/target.html";
+ RegisterResponse(prefetch_path, ResponseEntry("<body></body>"));
+ RegisterResponse(target_path,
+ ResponseEntry("<head><title>Target</title></head>",
+ /*content_types=*/""));
+ RegisterRequestHandler(embedded_test_server());
+
+ base::RunLoop prefetch_waiter;
+ auto request_counter = RequestCounter::CreateAndMonitor(
+ cross_origin_server_.get(), target_path, &prefetch_waiter);
+ RegisterRequestHandler(cross_origin_server_.get());
+ ASSERT_TRUE(embedded_test_server()->Start());
+ ASSERT_TRUE(cross_origin_server_->Start());
+
+ EXPECT_TRUE(
+ NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
+
+ const GURL target_url =
+ cross_origin_server_->GetURL("3p.example", target_path);
+
+ mojo::Remote<network::mojom::URLLoaderFactory> factory =
+ BindFactoryForMainFrame();
+
+ network::ResourceRequest request =
+ CreateCrossOriginPrefetchRequest(target_url);
+ request.load_flags |= net::LOAD_SUPPORT_ASYNC_REVALIDATION;
+
+ network::TestURLLoaderClient client;
+ mojo::Remote<network::mojom::URLLoader> loader;
+ factory->CreateLoaderAndStart(
+ loader.BindNewPipeAndPassReceiver(), /*request_id=*/0,
+ network::mojom::kURLLoadOptionNone, request, client.CreateRemote(),
+ net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+
+ client.RunUntilComplete();
+ EXPECT_EQ(net::OK, client.completion_status().error_code);
+ prefetch_waiter.Run();
+ EXPECT_EQ(1, request_counter->GetRequestCount());
+}
+
class FencedFramePrefetchTest : public PrefetchBrowserTestBase {
public:
FencedFramePrefetchTest()
Original Bug Report
Renderer Bypass of Network Load Flag Restrictions via Cross-Origin Prefetch
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 security team.
Overview: A compromised renderer can bypass network service load_flags restrictions by exploiting the cross-origin prefetch mechanism. The browser incorrectly forwards renderer-supplied load flags to a trusted CorsURLLoaderFactory without sanitization, enabling the renderer to set restricted internal flags.
Affected files:
content/browser/loader/prefetch_url_loader_service_context.cccontent/browser/url_loader_factory_params_helper.ccservices/network/cors/cors_url_loader_factory.cccontent/browser/loader/prefetch_url_loader.cc
Estimated timestamp from git blame: 2024-08-21
Vulnerability Details
When a renderer process initiates a cross-origin prefetch, it sends a network::ResourceRequest to the browser process via SubresourceProxyingURLLoaderService. If the request includes the net::LOAD_RESTRICTED_PREFETCH_FOR_MAIN_FRAME flag, it is routed to PrefetchURLLoaderServiceContext::CreatePrefetchLoaderAndStart.
To properly handle cache partitioning for these cross-origin prefetches, the browser process provisions a special URLLoaderFactory using RenderFrameHostImpl::CreateCrossOriginPrefetchLoaderFactoryBundle(). This factory is created via URLLoaderFactoryParamsHelper::CreateForPrefetch, which explicitly sets is_trusted = true to allow it to supply per-request IsolationInfo.
The vulnerability exists because PrefetchURLLoaderServiceContext copies the renderer’s ResourceRequest and forwards it to this trusted factory without sanitizing the load_flags. The validation function PrefetchURLLoaderServiceContext::IsValidCrossOriginPrefetch checks the initiator and ensures LOAD_CAN_USE_RESTRICTED_PREFETCH_FOR_MAIN_FRAME is not set, but leaves all other flags intact.
When the request reaches the Network Service, CorsURLLoaderFactory::IsValidRequest performs a security check to ensure untrusted callers are not using restricted load flags. However, because this specific CorsURLLoaderFactory was created with is_trusted_ = true, the allowlist check is completely bypassed:
// services/network/cors/cors_url_loader_factory.cc
if (!is_trusted_) {
...
// Apply allowlist for which flags untrusted factories are allowed to use.
if (request.load_flags & ~(... allowlist ...)) {
mojo::ReportBadMessage("CorsURLLoaderFactory: Untrusted caller using restricted load flag");
return false;
}
}
As a result, a compromised renderer can inject highly restricted internal network flags into the prefetch request.
Impact
A compromised renderer can bypass critical network-level security policies:
net::LOAD_BYPASS_PROXY: The request will bypass Enterprise DLP, inspection, or user-configured proxy settings, routing directly to the target host.net::LOAD_SHOULD_BYPASS_HSTS: By also settingcredentials_mode = network::mojom::CredentialsMode::kOmit, the attacker can satisfy theCHECKinURLRequestHttpJoband force the request to bypass HSTS upgrades. This allows plaintext HTTP requests to HSTS-enrolled domains.net::LOAD_DISABLE_CERT_NETWORK_FETCHES: The attacker can disable OCSP/CRL/AIA fetches during certificate verification.
While the PrefetchURLLoader drains the response body before it reaches the renderer, these network side-effects represent a significant trust-boundary violation and policy bypass.
Suggested Steps to Trigger (Theoretical)
- Gain code execution in a sandboxed renderer process.
- Construct a
network::ResourceRequesttargeting a cross-origin URL. - Set
load_flagsto includenet::LOAD_PREFETCH | net::LOAD_RESTRICTED_PREFETCH_FOR_MAIN_FRAME | net::LOAD_BYPASS_PROXY. - To bypass HSTS, also include
net::LOAD_SHOULD_BYPASS_HSTSand setcredentials_mode = network::mojom::CredentialsMode::kOmit. - Send the request to the browser process via the
SubresourceProxyingURLLoaderServiceMojo interface. - The request will bypass
CorsURLLoaderFactoryrestrictions and be executed by the network stack with the malicious flags applied.
Suggested Fix
In PrefetchURLLoaderServiceContext::CreatePrefetchLoaderAndStart or IsValidCrossOriginPrefetch, the browser process must strictly sanitize resource_request.load_flags. It should either apply a bitwise AND mask against an allowlist of permitted prefetch flags, or immediately reject the request if any restricted flags are present.
Evaluated with Chrome root at commit: e9e0fcbb690b1a8c1a26c81c2a9ea23d6e178368
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.