Medium watch CVSS 6.8 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Network
DescriptionImproper input validation in Network
ComponentNetwork
Sub-componentLoader
Bug ClassInput Validation
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.
Tracker498328139
Fix commit477cd38a3be5 (chromium/src) +183/-7
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-31 (per vendor advisory)
Fix landed2026-07-03 (public git)
Disclosed2026-08-25
Patch gap53 days — fix public in source before the advisory

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 CorsURLLoaderFactory validates requests from untrusted (renderer) factories, including an allowlist on load_flags, but skips that allowlist for factories created with is_trusted = true.
`load_flags`
a bitmask of net::LOAD_* values that alter network-stack behaviour; some (for example net::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.

Key insight
The browser re-originated a renderer-controlled request on a trusted factory without re-applying the validation that the trusted factory skips; the fix tightens the browser-side prefetch path by enforcing the same untrusted-caller load_flags allowlist before forwarding.

Attack Path

  1. 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).
  2. Prefetch IPC The renderer sends a cross-origin (or recursive) prefetch ResourceRequest to the frame’s SubresourceProxyingURLLoaderService factory with net::LOAD_PREFETCH plus a restricted flag such as net::LOAD_BYPASS_PROXY or net::LOAD_DISABLE_CERT_NETWORK_FETCHES.
  3. Trusted forwarding Pre-patch, PrefetchURLLoaderServiceContext checked only trusted_params and forwarded the request to a trusted CorsURLLoaderFactory, which does not apply the load_flags allowlist.
  4. 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

A compromised renderer could cause the network service to issue prefetch requests with browser-only 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

FunctionChangeNotes
COMPONENT_EXPORT
services/network/public/cpp/resource_request.h
modified
PrefetchLoadFlagsBrowserTest
content/browser/loader/prefetch_browsertest.cc
modified
FencedFramePrefetchTest
content/browser/loader/prefetch_browsertest.cc
modified

Files Changed

  • content/browser/loader/prefetch_url_loader_service_context.cc
  • services/network/cors/cors_url_loader_factory.cc
  • content/browser/loader/prefetch_browsertest.cc
  • services/network/public/cpp/resource_request.h
  • services/network/public/cpp/resource_request.cc

Audit Directions

  • Renderer requests re-issued on trusted factories
    Audit every browser-side proxy (prefetch, service worker, keep-alive, signed exchange, Web Bundles) that takes a renderer ResourceRequest and forwards it to an is_trusted factory; each must replicate CorsURLLoaderFactory::IsValidRequest-style checks on load_flags, trusted_params and similar fields.
  • Duplicated allowlists
    Look 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 privilege
    Grep for consumers of net::LOAD_BYPASS_PROXY, net::LOAD_DISABLE_CERT_NETWORK_FETCHES and other browser-only bits to confirm no renderer-reachable path can set them.
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()
Loading diff…

Regression Test / PoC

shipped with the fix
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()
Loading diff…

Original Bug Report

reported by rj...@google.com

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.cc
  • content/browser/url_loader_factory_params_helper.cc
  • services/network/cors/cors_url_loader_factory.cc
  • content/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:

  1. net::LOAD_BYPASS_PROXY: The request will bypass Enterprise DLP, inspection, or user-configured proxy settings, routing directly to the target host.
  2. net::LOAD_SHOULD_BYPASS_HSTS: By also setting credentials_mode = network::mojom::CredentialsMode::kOmit, the attacker can satisfy the CHECK in URLRequestHttpJob and force the request to bypass HSTS upgrades. This allows plaintext HTTP requests to HSTS-enrolled domains.
  3. 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)

  1. Gain code execution in a sandboxed renderer process.
  2. Construct a network::ResourceRequest targeting a cross-origin URL.
  3. Set load_flags to include net::LOAD_PREFETCH | net::LOAD_RESTRICTED_PREFETCH_FOR_MAIN_FRAME | net::LOAD_BYPASS_PROXY.
  4. To bypass HSTS, also include net::LOAD_SHOULD_BYPASS_HSTS and set credentials_mode = network::mojom::CredentialsMode::kOmit.
  5. Send the request to the browser process via the SubresourceProxyingURLLoaderService Mojo interface.
  6. The request will bypass CorsURLLoaderFactory restrictions 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.

View on issue tracker