Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Network
DescriptionInsufficient validation of untrusted input in Network
ComponentNetwork
Bug ClassLogic Error
Tracker496169594
Fix commit5ee8fb48bc29 (chromium/src) +61/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
TEST_F
content/browser/interest_group/ad_auction_url_loader_interceptor_unittest.cc
modified
if
content/browser/loader/subresource_proxying_url_loader.cc
modified
for
content/browser/loader/subresource_proxying_url_loader.cc
modified

Files Changed

  • content/browser/interest_group/ad_auction_url_loader_interceptor_unittest.cc
  • content/browser/loader/subresource_proxying_url_loader.cc
  • content/browser/loader/subresource_proxying_url_loader.h
From 5ee8fb48bc29eb8659b816fb3ac585eb674365ae Mon Sep 17 00:00:00 2001
From: Yao Xiao <yaoxia@chromium.org>
Date: Thu, 26 Mar 2026 10:24:57 -0700
Subject: [PATCH] SubresourceProxyingURLLoader: Add state validation for FollowRedirect

This adds a `redirect_pending_` flag to SubresourceProxyingURLLoader to
ensure that FollowRedirect() can only be called after a legitimate
redirect has been received via OnReceiveRedirect().

Previously, a compromised renderer could send an unsolicited
FollowRedirect IPC at any time. This allowed it to:
1. Trigger a CHECK failure in
   AdAuctionURLLoaderInterceptor::WillFollowRedirect, crashing the
   browser process.
2. Poison the origin in BrowsingTopicsURLLoaderInterceptor by racing
   FollowRedirect with OnReceiveResponse, potentially attributing
   Topics observations to a victim origin.

Fixed by reporting a bad message if FollowRedirect is called when no
redirect is pending.

Bug: 496169594
Change-Id: Iae71c56cec8ce7919d728f247778906a0cff761c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7701154
Reviewed-by: mmenke <mmenke@chromium.org>
Commit-Queue: Yao Xiao <yaoxia@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1605638}
---

diff --git a/content/browser/interest_group/ad_auction_url_loader_interceptor_unittest.cc b/content/browser/interest_group/ad_auction_url_loader_interceptor_unittest.cc
index f56d977cf..f7871827 100644
--- a/content/browser/interest_group/ad_auction_url_loader_interceptor_unittest.cc
+++ b/content/browser/interest_group/ad_auction_url_loader_interceptor_unittest.cc
@@ -1013,4 +1013,42 @@
               ::testing::IsEmpty());
 }
 
+TEST_F(AdAuctionURLLoaderInterceptorTest, UnsolicitedFollowRedirect) {
+  NavigatePage(GURL("https://google.com"));
+
+  mojo::Remote<network::mojom::URLLoaderFactory> remote_url_loader_factory;
+  network::TestURLLoaderFactory proxied_url_loader_factory;
+  mojo::Remote<network::mojom::URLLoader> remote_loader;
+  mojo::PendingReceiver<network::mojom::URLLoaderClient> client;
+
+  base::WeakPtr<SubresourceProxyingURLLoaderService::BindContext> bind_context =
+      CreateFactory(proxied_url_loader_factory, remote_url_loader_factory);
+  bind_context->OnDidCommitNavigation(
+      web_contents()->GetPrimaryMainFrame()->GetWeakDocumentPtr());
+
+  remote_url_loader_factory->CreateLoaderAndStart(
+      remote_loader.BindNewPipeAndPassReceiver(),
+      /*request_id=*/0, /*options=*/0,
+      CreateResourceRequest(GURL("https://foo1.com")),
+      client.InitWithNewPipeAndPassRemote(),
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+  remote_url_loader_factory.FlushForTesting();
+
+  std::string received_error;
+  mojo::SetDefaultProcessErrorHandler(base::BindLambdaForTesting(
+      [&](const std::string& error) { received_error = error; }));
+
+  // This should trigger ReportBadMessage in
+  // SubresourceProxyingURLLoader::FollowRedirect
+  remote_loader->FollowRedirect(/*removed_headers=*/{},
+                                /*modified_headers=*/{},
+                                /*modified_cors_exempt_headers=*/{},
+                                /*new_url=*/std::nullopt);
+  remote_loader.FlushForTesting();
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_EQ(received_error, "Unexpected FollowRedirect");
+  mojo::SetDefaultProcessErrorHandler(base::NullCallback());
+}
+
 }  // namespace content
diff --git a/content/browser/loader/subresource_proxying_url_loader.cc b/content/browser/loader/subresource_proxying_url_loader.cc
index 4de4b610..d2a87af0 100644
--- a/content/browser/loader/subresource_proxying_url_loader.cc
+++ b/content/browser/loader/subresource_proxying_url_loader.cc
@@ -6,6 +6,7 @@
 
 #include "content/browser/browsing_topics/browsing_topics_url_loader_interceptor.h"
 #include "content/browser/interest_group/ad_auction_url_loader_interceptor.h"
+#include "mojo/public/cpp/bindings/message.h"
 #include "services/network/public/cpp/shared_url_loader_factory.h"
 #include "services/network/public/mojom/early_hints.mojom.h"
 
@@ -61,6 +62,12 @@
     const net::HttpRequestHeaders& modified_headers,
     const net::HttpRequestHeaders& modified_cors_exempt_headers,
     const std::optional<GURL>& new_url) {
+  if (!redirect_pending_) {
+    mojo::ReportBadMessage("Unexpected FollowRedirect");
+    return;
+  }
+  redirect_pending_ = false;
+
   std::vector<std::string> new_removed_headers = removed_headers;
   net::HttpRequestHeaders new_modified_headers = modified_headers;
 
@@ -87,6 +94,11 @@
     network::mojom::URLResponseHeadPtr head,
     mojo::ScopedDataPipeConsumerHandle body,
     std::optional<mojo_base::BigBuffer> cached_metadata) {
+  // Reset the redirect state. While it's unclear if a redirect can genuinely
+  // be pending at this point, we clear it to be robust against variations
+  // in URLLoader behavior (e.g., notifications of failures during redirects).
+  redirect_pending_ = false;
+
   for (auto& interceptor : interceptors_) {
     interceptor->OnReceiveResponse(head);
   }
@@ -98,6 +110,8 @@
 void SubresourceProxyingURLLoader::OnReceiveRedirect(
     const net::RedirectInfo& redirect_info,
     network::mojom::URLResponseHeadPtr head) {
+  redirect_pending_ = true;
+
   for (auto& interceptor : interceptors_) {
     interceptor->OnReceiveRedirect(redirect_info, head);
   }
@@ -120,6 +134,11 @@
 
 void SubresourceProxyingURLLoader::OnComplete(
     const network::URLLoaderCompletionStatus& status) {
+  // Reset the redirect state. While it's unclear if a redirect can genuinely
+  // be pending at this point, we clear it to be robust against variations
+  // in URLLoader behavior (e.g., notifications of failures during redirects).
+  redirect_pending_ = false;
+
   forwarding_client_->OnComplete(status);
 }
 
diff --git a/content/browser/loader/subresource_proxying_url_loader.h b/content/browser/loader/subresource_proxying_url_loader.h
index 8b8c543bc..c2b6ac22 100644
--- a/content/browser/loader/subresource_proxying_url_loader.h
+++ b/content/browser/loader/subresource_proxying_url_loader.h
@@ -113,6 +113,10 @@
   std::vector<std::unique_ptr<Interceptor>> interceptors_;
 
   mojo::Receiver<network::mojom::URLLoaderClient> client_receiver_{this};
+
+  // Whether a redirect is currently pending. If true, the next call from the
+  // renderer should be FollowRedirect().
+  bool redirect_pending_ = false;
 };
 
 }  // namespace content
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/interest_group/ad_auction_url_loader_interceptor_unittest.cc b/content/browser/interest_group/ad_auction_url_loader_interceptor_unittest.cc
index f56d977cf..f7871827 100644
--- a/content/browser/interest_group/ad_auction_url_loader_interceptor_unittest.cc
+++ b/content/browser/interest_group/ad_auction_url_loader_interceptor_unittest.cc
@@ -1013,4 +1013,42 @@
               ::testing::IsEmpty());
 }
 
+TEST_F(AdAuctionURLLoaderInterceptorTest, UnsolicitedFollowRedirect) {
+  NavigatePage(GURL("https://google.com"));
+
+  mojo::Remote<network::mojom::URLLoaderFactory> remote_url_loader_factory;
+  network::TestURLLoaderFactory proxied_url_loader_factory;
+  mojo::Remote<network::mojom::URLLoader> remote_loader;
+  mojo::PendingReceiver<network::mojom::URLLoaderClient> client;
+
+  base::WeakPtr<SubresourceProxyingURLLoaderService::BindContext> bind_context =
+      CreateFactory(proxied_url_loader_factory, remote_url_loader_factory);
+  bind_context->OnDidCommitNavigation(
+      web_contents()->GetPrimaryMainFrame()->GetWeakDocumentPtr());
+
+  remote_url_loader_factory->CreateLoaderAndStart(
+      remote_loader.BindNewPipeAndPassReceiver(),
+      /*request_id=*/0, /*options=*/0,
+      CreateResourceRequest(GURL("https://foo1.com")),
+      client.InitWithNewPipeAndPassRemote(),
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+  remote_url_loader_factory.FlushForTesting();
+
+  std::string received_error;
+  mojo::SetDefaultProcessErrorHandler(base::BindLambdaForTesting(
+      [&](const std::string& error) { received_error = error; }));
+
+  // This should trigger ReportBadMessage in
+  // SubresourceProxyingURLLoader::FollowRedirect
+  remote_loader->FollowRedirect(/*removed_headers=*/{},
+                                /*modified_headers=*/{},
+                                /*modified_cors_exempt_headers=*/{},
+                                /*new_url=*/std::nullopt);
+  remote_loader.FlushForTesting();
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_EQ(received_error, "Unexpected FollowRedirect");
+  mojo::SetDefaultProcessErrorHandler(base::NullCallback());
+}
+
 }  // namespace content
Loading diff…

Original Bug Report

reported by vi...@google.com

Potential origin spoofing and DoS via unsolicited FollowRedirect

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A compromised renderer can send an unsolicited FollowRedirect IPC to SubresourceProxyingURLLoader before any network redirect actually occurs. This lack of state validation allows the renderer to trigger a deterministic CHECK failure in AdAuctionURLLoaderInterceptor, crashing the browser. Additionally, by racing this IPC with a legitimate network response, an attacker can poison the origin in BrowsingTopicsURLLoaderInterceptor to record fraudulent Topics API observations.

Affected files:

  • content/browser/loader/subresource_proxying_url_loader.cc
  • content/browser/browsing_topics/browsing_topics_url_loader_interceptor.cc
  • content/browser/interest_group/ad_auction_url_loader_interceptor.cc

Estimated timestamp from git blame: 2023-05-17

Note: This vulnerability was discovered via static analysis by an AI agent. The steps below are theoretical and suggest how an attacker might exploit the issue, as a working Proof of Concept has not yet been executed.

Root Cause

The SubresourceProxyingURLLoader acts as a proxy between the renderer and the Network Service to intercept subresource requests for features like Ad Auctions and Browsing Topics.

When a renderer calls FollowRedirect on its URLLoader Mojo remote, SubresourceProxyingURLLoader::FollowRedirect unconditionally iterates over its interceptors and calls interceptor->WillFollowRedirect(...). Crucially, it never validates whether the underlying network request actually issued a redirect (e.g., by tracking if OnReceiveRedirect was called first). This allows a compromised renderer to arbitrarily invoke WillFollowRedirect at any point in the loader’s lifecycle.

Impact 1: Deterministic Browser Process Crash (DoS)

In AdAuctionURLLoaderInterceptor, the ad_auction_headers_eligible_ flag is set to true in WillStartRequest. The code assumes OnReceiveRedirect will safely clear this flag before WillFollowRedirect is ever called.

Because the renderer can invoke FollowRedirect out-of-order, it reaches AdAuctionURLLoaderInterceptor::WillFollowRedirect while the flag is still true. This hits CHECK(!ad_auction_headers_eligible_) at content/browser/interest_group/ad_auction_url_loader_interceptor.cc:73, immediately crashing the browser process.

Impact 2: Browsing Topics Origin Poisoning

In BrowsingTopicsURLLoaderInterceptor, WillFollowRedirect updates its internal url_ state to the attacker-supplied new_url.

Because the renderer’s URLLoader Mojo pipe and the Network Service’s URLLoaderClient Mojo pipe are independent, an attacker can race an unsolicited FollowRedirect IPC against a legitimate OnReceiveResponse IPC from their own server. If the browser processes the FollowRedirect (poisoning url_ to a victim’s origin) just before the OnReceiveResponse (containing Observe-Browsing-Topics: ?1), the browser will erroneously attribute the Topics observation to the victim’s origin instead of the attacker’s.

Potential Attacker Steps

To trigger the DoS:

  1. Compromise a renderer process.
  2. Initiate a subresource fetch with adAuctionHeaders: true.
  3. Immediately send a FollowRedirect Mojo IPC on the returned URLLoader remote.
  4. The browser process hits the CHECK and crashes.

To trigger Origin Poisoning:

  1. Compromise a renderer process.
  2. Initiate a subresource fetch with browsing_topics: true to an attacker-controlled server.
  3. The attacker’s server responds with Observe-Browsing-Topics: ?1.
  4. The compromised renderer sends an unsolicited FollowRedirect IPC with new_url set to https://victim.com, timed to arrive at the browser process just before the Network Service’s OnReceiveResponse IPC.
  5. The browser processes FollowRedirect, updating the interceptor’s url_ to https://victim.com.
  6. The browser processes OnReceiveResponse and records the Topics observation for https://victim.com.

Suggested Fix

SubresourceProxyingURLLoader should track whether a redirect is currently pending.

  1. Add a bool redirect_pending_ = false; member to SubresourceProxyingURLLoader.
  2. Set redirect_pending_ = true; inside SubresourceProxyingURLLoader::OnReceiveRedirect.
  3. Inside SubresourceProxyingURLLoader::FollowRedirect, verify that redirect_pending_ is true. If it is false, terminate the request, do not forward the call to interceptors, and optionally call mojo::ReportBadMessage("Unexpected FollowRedirect") to kill the misbehaving renderer.
  4. Reset redirect_pending_ = false; after successfully processing a valid FollowRedirect.

Evaluated with Chrome root at commit: bb48272cafb7e24c93f55ef40da398cd206ee651


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. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker
Links in the report