CVE-2026-79032
Overview
Background
- `SubresourceProxyingURLLoaderService` / `PrefetchURLLoaderServiceContext`
- browser-process services that receive renderer-originated
network::ResourceRequests for<link rel=prefetch>-style loads and proxy them to the network service. - Trusted vs. untrusted `URLLoaderFactory`
- the network service’s
CorsURLLoaderFactoryvalidates requests from untrusted (renderer) factories, including an allowlist onload_flags, but skips that allowlist for factories created withis_trusted = true. - `load_flags`
- a bitmask of
net::LOAD_*values that alter network-stack behaviour; some (for examplenet::LOAD_BYPASS_PROXY,net::LOAD_DISABLE_CERT_NETWORK_FETCHES) are meant only for privileged browser-side callers. - `ReportBadMessage`
- the Mojo mechanism by which the browser flags (and normally terminates) a renderer that sent an invalid IPC.
Root Cause Analysis
Per the commit message, PrefetchURLLoaderServiceContext receives renderer-supplied ResourceRequests via SubresourceProxyingURLLoaderService, and for cross-origin and recursive prefetches forwards them to a CorsURLLoaderFactory created with is_trusted = true (via URLLoaderFactoryParamsHelper::CreateForPrefetch). Because that factory is trusted, CorsURLLoaderFactory’s untrusted-caller load_flags allowlist is never applied, and the browser-side prefetch path had only a trusted_params check, not a load_flags check. The invariant “renderer-originated requests may only carry renderer-permitted load flags” was therefore broken: a renderer could launder restricted flags such as net::LOAD_BYPASS_PROXY or net::LOAD_DISABLE_CERT_NETWORK_FETCHES through the browser into a trusted network request.
The fix factors the existing allowlist into a new shared helper, network::GetAllowedLoadFlagsForUntrustedRequests(), and in the prefetch context rejects any request whose load_flags contain bits outside that set — reporting a bad message and completing the client with net::ERR_INVALID_ARGUMENT before the request is forwarded. The CorsURLLoaderFactory change is a pure refactor to use the same helper (the flag set is identical), so both enforcement points now share one definition.
load_flags allowlist before forwarding.Attack Path
- Renderer control
An attacker needs a compromised renderer (ordinary web content cannot set arbitrary
net::LOAD_*bits; the regression test drives the Mojo factory directly). - Prefetch IPC
The renderer sends a cross-origin (or recursive) prefetch
ResourceRequestto the frame’sSubresourceProxyingURLLoaderServicefactory withnet::LOAD_PREFETCHplus a restricted flag such asnet::LOAD_BYPASS_PROXYornet::LOAD_DISABLE_CERT_NETWORK_FETCHES. - Trusted forwarding
Pre-patch,
PrefetchURLLoaderServiceContextchecked onlytrusted_paramsand forwarded the request to a trustedCorsURLLoaderFactory, which does not apply theload_flagsallowlist. - Privileged network behaviour The network service honours the restricted flag, e.g. sending the request directly rather than via the configured proxy, or altering certificate-related network fetches (inferred consequences of the named flags).
Impact Assessment
load_flags, bypassing policy such as configured proxies or certificate-fetch behaviour; the concrete consequences depend on the flag chosen (for example deanonymisation by bypassing a proxy is inferred, not shown in the patch). The bug requires a compromised renderer and lives in the browser/network-service trust boundary; it is not a memory-corruption bug. The advisory classifies it only as “Improper input validation in Network” and does not state in-the-wild exploitation.Changed Functions
| Function | Change | Notes |
|---|---|---|
COMPONENT_EXPORTservices/network/public/cpp/resource_request.h |
modified | |
PrefetchLoadFlagsBrowserTestcontent/browser/loader/prefetch_browsertest.cc |
modified | |
FencedFramePrefetchTestcontent/browser/loader/prefetch_browsertest.cc |
modified |
Files Changed
content/browser/loader/prefetch_url_loader_service_context.ccservices/network/cors/cors_url_loader_factory.cccontent/browser/loader/prefetch_browsertest.ccservices/network/public/cpp/resource_request.hservices/network/public/cpp/resource_request.cc
Audit Directions
- Renderer requests re-issued on trusted factoriesAudit every browser-side proxy (prefetch, service worker, keep-alive, signed exchange, Web Bundles) that takes a renderer
ResourceRequestand forwards it to anis_trustedfactory; each must replicateCorsURLLoaderFactory::IsValidRequest-style checks onload_flags,trusted_paramsand similar fields. - Duplicated allowlistsLook for other security allowlists copied inline in multiple places (headers, request modes, credentials modes) that could diverge; consolidating them as this fix did reduces bypass risk.
- Flags implying privilegeGrep for consumers of
net::LOAD_BYPASS_PROXY,net::LOAD_DISABLE_CERT_NETWORK_FETCHESand other browser-only bits to confirm no renderer-reachable path can set them.
Patch
From 477cd38a3be5889a17d121a58bfc4f18bfbc0cee Mon Sep 17 00:00:00 2001
From: Shunya Shishido <sisidovski@chromium.org>
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 <bashi@chromium.org>
Reviewed-by: Takashi Toyoshima <toyoshim@chromium.org>
Commit-Queue: Shunya Shishido <sisidovski@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1656322}
---
diff --git a/content/browser/loader/prefetch_url_loader_service_context.cc b/content/browser/loader/prefetch_url_loader_service_context.cc
index dc5c7b5..779902a 100644
--- a/content/browser/loader/prefetch_url_loader_service_context.cc
+++ b/content/browser/loader/prefetch_url_loader_service_context.cc
@@ -68,6 +68,20 @@
return;
}
+ // The request originated in a renderer, but for cross-origin and recursive
+ // prefetches it is forwarded to a trusted network loader factory which does
+ // not validate `load_flags`. Apply the same allowlist that
+ // `CorsURLLoaderFactory::IsValidRequest()` applies to untrusted callers.
+ if (resource_request.load_flags &
+ ~network::GetAllowedLoadFlagsForUntrustedRequests()) {
+ loader_factory_receivers_->ReportBadMessage(
+ "Prefetch/CreatePrefetchLoaderAndStart: restricted load flag");
+ mojo::Remote<network::mojom::URLLoaderClient>(std::move(client))
+ ->OnComplete(
+ network::URLLoaderCompletionStatus(net::ERR_INVALID_ARGUMENT));
+ return;
+ }
+
if (resource_request.load_flags &
net::LOAD_RESTRICTED_PREFETCH_FOR_MAIN_FRAME) {
CHECK(!resource_request.recursive_prefetch_token);
diff --git a/services/network/cors/cors_url_loader_factory.cc b/services/network/cors/cors_url_loader_factory.cc
index 82cc691..dd0d2c4b 100644
--- a/services/network/cors/cors_url_loader_factory.cc
+++ b/services/network/cors/cors_url_loader_factory.cc
@@ -638,13 +638,7 @@
}
// Apply allowlist for which flags untrusted factories are allowed to use.
- if (request.load_flags &
- ~(net::LOAD_VALIDATE_CACHE | net::LOAD_BYPASS_CACHE |
- net::LOAD_SKIP_CACHE_VALIDATION | net::LOAD_ONLY_FROM_CACHE |
- net::LOAD_DISABLE_CACHE | net::LOAD_PREFETCH |
- net::LOAD_IGNORE_LIMITS | net::LOAD_DO_NOT_USE_EMBEDDED_IDENTITY |
- net::LOAD_SUPPORT_ASYNC_REVALIDATION |
- net::LOAD_RESTRICTED_PREFETCH_FOR_MAIN_FRAME)) {
+ if (request.load_flags & ~GetAllowedLoadFlagsForUntrustedRequests()) {
mojo::ReportBadMessage(
"CorsURLLoaderFactory: Untrusted caller using restricted load flag");
return false;
diff --git a/services/network/public/cpp/resource_request.cc b/services/network/public/cpp/resource_request.cc
index a3f0786..2a92c36 100644
--- a/services/network/public/cpp/resource_request.cc
+++ b/services/network/public/cpp/resource_request.cc
@@ -442,6 +442,15 @@
NOTREACHED();
}
+int GetAllowedLoadFlagsForUntrustedRequests() {
+ return net::LOAD_VALIDATE_CACHE | net::LOAD_BYPASS_CACHE |
+ net::LOAD_SKIP_CACHE_VALIDATION | net::LOAD_ONLY_FROM_CACHE |
+ net::LOAD_DISABLE_CACHE | net::LOAD_PREFETCH |
+ net::LOAD_IGNORE_LIMITS | net::LOAD_DO_NOT_USE_EMBEDDED_IDENTITY |
+ net::LOAD_SUPPORT_ASYNC_REVALIDATION |
+ net::LOAD_RESTRICTED_PREFETCH_FOR_MAIN_FRAME;
+}
+
namespace debug {
ScopedResourceRequestCrashKeys::ScopedResourceRequestCrashKeys(
diff --git a/services/network/public/cpp/resource_request.h b/services/network/public/cpp/resource_request.h
index 304c11d..a6f38eb6 100644
--- a/services/network/public/cpp/resource_request.h
+++ b/services/network/public/cpp/resource_request.h
@@ -314,6 +314,11 @@
net::ReferrerPolicy ReferrerPolicyForUrlRequest(
mojom::ReferrerPolicy referrer_policy);
+// Returns a bitmask of net::LOAD_* flags that are allowed for requests from
+// untrusted processes.
+COMPONENT_EXPORT(NETWORK_CPP_BASE)
+int GetAllowedLoadFlagsForUntrustedRequests();
+
namespace debug {
class COMPONENT_EXPORT(NETWORK_CPP_BASE) ScopedResourceRequestCrashKeys {
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()
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.