Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Extensions
DescriptionInsufficient validation of untrusted input in Extensions
ComponentExtensions
Bug ClassLogic Error
Tracker513564337
Fix commitef7d0a803c37 (chromium/src) +233/-200
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-08

Changed Functions

FunctionChangeNotes
if
extensions/browser/api/web_request/web_request_api.cc
modified
navigation_response_task_runner_
extensions/browser/api/web_request/web_request_proxying_url_loader_factory.cc
modified

Files Changed

  • extensions/browser/api/web_request/web_request_api.cc
  • extensions/browser/api/web_request/web_request_api.h
  • extensions/browser/api/web_request/web_request_proxying_url_loader_factory.cc
From ef7d0a803c37c6efe4d4278ab1aadf205b94271a Mon Sep 17 00:00:00 2001
From: Andrea Orru <[email protected]>
Date: Fri, 24 Jul 2026 15:27:02 -0700
Subject: [PATCH] [Extensions] Rename request IDs in WebRequestProxyingURLLoaderFactory

Disambiguate request ID parameters and members in WebRequestAPI and
WebRequestProxyingURLLoaderFactory:

- Rename `client_request_id` to `request_id_from_client` to clearly
  denote that the ID is supplied by the upstream caller of
  `CreateLoaderAndStart()`, avoiding ambiguity with downstream
  interfaces such as `URLLoaderClient`.

- Rename `network_service_request_id` to
  `request_id_for_network_service` to make explicit that this ID is
  assigned by the proxy and forwarded downstream to the network service.

- Rename `request_id` to `browser_context_request_id` to make explicit
  that this ID is unique per BrowserContext and avoid ambiguity with
  other shorthanded request IDs.

Bug: 513564337, 521887333
Change-Id: I47dfafbf798bbe0506c1564461f588434819580d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8134541
Reviewed-by: Devlin Cronin <[email protected]>
Commit-Queue: Andrea Orru <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1668200}
---

diff --git a/extensions/browser/api/web_request/web_request_api.cc b/extensions/browser/api/web_request/web_request_api.cc
index 19954b17..8c1e8c3 100644
--- a/extensions/browser/api/web_request/web_request_api.cc
+++ b/extensions/browser/api/web_request/web_request_api.cc
@@ -389,10 +389,11 @@
 WebRequestAPI::RequestIDGenerator::RequestIDGenerator() = default;
 WebRequestAPI::RequestIDGenerator::~RequestIDGenerator() = default;
 
-int64_t WebRequestAPI::RequestIDGenerator::Generate(int32_t routing_id,
-                                                    int32_t client_request_id) {
+int64_t WebRequestAPI::RequestIDGenerator::Generate(
+    int32_t routing_id,
+    int32_t request_id_from_client) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
-  auto it = saved_id_map_.find({routing_id, client_request_id});
+  auto it = saved_id_map_.find({routing_id, request_id_from_client});
   if (it != saved_id_map_.end()) {
     int64_t id = it->second;
     saved_id_map_.erase(it);
@@ -402,12 +403,12 @@
 }
 
 void WebRequestAPI::RequestIDGenerator::SaveID(int32_t routing_id,
-                                               int32_t client_request_id,
+                                               int32_t request_id_from_client,
                                                uint64_t request_id) {
-  // If `client_request_id` is 0, we cannot reliably match the generated ID to a
-  // restarted request, so ignore it.
-  if (client_request_id != 0) {
-    saved_id_map_.insert({{routing_id, client_request_id}, request_id});
+  // If `request_id_from_client` is 0, we cannot reliably match the generated
+  // ID to a restarted request, so ignore it.
+  if (request_id_from_client != 0) {
+    saved_id_map_.insert({{routing_id, request_id_from_client}, request_id});
   }
 }
 
diff --git a/extensions/browser/api/web_request/web_request_api.h b/extensions/browser/api/web_request/web_request_api.h
index 76b78788..9832015 100644
--- a/extensions/browser/api/web_request/web_request_api.h
+++ b/extensions/browser/api/web_request/web_request_api.h
@@ -166,15 +166,16 @@
     ~RequestIDGenerator();
 
     // Generates a WebRequest ID. If `SaveID()` was previously called with the
-    // same (`routing_id`, `client_request_id`) pair, returns the saved ID and
-    // removes the mapping. Otherwise, generates and returns a new unique ID.
-    int64_t Generate(int32_t routing_id, int32_t client_request_id);
+    // same (`routing_id`, `request_id_from_client`) pair, returns the saved ID
+    // and removes the mapping. Otherwise, generates and returns a new unique
+    // ID.
+    int64_t Generate(int32_t routing_id, int32_t request_id_from_client);
 
-    // Maps a WebRequest ID to a (`routing_id`, `client_request_id`) pair when a
-    // request is restarted. Callers must subsequently call `Generate()` with
-    // the same pair to reclaim the ID and prevent memory leaks.
+    // Maps a WebRequest ID to a (`routing_id`, `request_id_from_client`) pair
+    // when a request is restarted. Callers must subsequently call `Generate()`
+    // with the same pair to reclaim the ID and prevent memory leaks.
     void SaveID(int32_t routing_id,
-                int32_t client_request_id,
+                int32_t request_id_from_client,
                 uint64_t request_id);
 
     // Generates a non-zero request ID to forward to the network service for
diff --git a/extensions/browser/api/web_request/web_request_proxying_url_loader_factory.cc b/extensions/browser/api/web_request/web_request_proxying_url_loader_factory.cc
index cf5998e2..ac1f5c0f 100644
--- a/extensions/browser/api/web_request/web_request_proxying_url_loader_factory.cc
+++ b/extensions/browser/api/web_request/web_request_proxying_url_loader_factory.cc
@@ -141,9 +141,9 @@
 
 WebRequestProxyingURLLoaderFactory::InProgressRequest::InProgressRequest(
     WebRequestProxyingURLLoaderFactory* factory,
-    uint64_t request_id,
-    int32_t network_service_request_id,
-    int32_t client_request_id,
+    uint64_t profile_request_id,
+    int32_t request_id_for_network_service,
+    int32_t request_id_from_client,
     int32_t view_routing_id,
     int32_t frame_routing_id,
     uint32_t options,
@@ -156,9 +156,9 @@
     : factory_(factory),
       request_(request),
       original_initiator_(request.request_initiator),
-      request_id_(request_id),
-      network_service_request_id_(network_service_request_id),
-      client_request_id_(client_request_id),
+      profile_request_id_(profile_request_id),
+      request_id_for_network_service_(request_id_for_network_service),
+      request_id_from_client_(request_id_from_client),
       view_routing_id_(view_routing_id),
       frame_routing_id_(frame_routing_id),
       options_(options),
@@ -170,19 +170,20 @@
       target_client_(std::move(client)),
       current_response_(network::mojom::URLResponseHead::New()),
       has_any_extra_headers_listeners_(
-          network_service_request_id_ != 0 &&
+          request_id_for_network_service_ != 0 &&
           WebRequestEventRouter::Get(factory_->browser_context_)
               ->HasAnyExtraHeadersListener(factory_->browser_context_)),
       has_any_security_info_listeners_(
           WebRequestEventRouter::Get(factory_->browser_context_)
               ->HasAnySecurityInfoListener(factory_->browser_context_)),
       navigation_response_task_runner_(navigation_response_task_runner) {
-  TRACE_EVENT("extensions",
-              "WebRequestProxyingURLLoaderFactory::InProgressRequest::"
-              "InProgressRequest",
-              perfetto::Flow::ProcessScoped(
-                  request_id_, kWebRequestProxyingURLLoaderFactoryScope),
-              "url", request.url.spec());
+  TRACE_EVENT(
+      "extensions",
+      "WebRequestProxyingURLLoaderFactory::InProgressRequest::"
+      "InProgressRequest",
+      perfetto::Flow::ProcessScoped(profile_request_id_,
+                                    kWebRequestProxyingURLLoaderFactoryScope),
+      "url", request.url.spec());
 
   // If there is a client error, clean up the request.
   target_client_.set_disconnect_handler(
@@ -197,13 +198,13 @@
 
 WebRequestProxyingURLLoaderFactory::InProgressRequest::InProgressRequest(
     WebRequestProxyingURLLoaderFactory* factory,
-    uint64_t request_id,
+    uint64_t profile_request_id,
     int32_t frame_routing_id,
     const network::ResourceRequest& request)
     : factory_(factory),
       request_(request),
       original_initiator_(request.request_initiator),
-      request_id_(request_id),
+      profile_request_id_(profile_request_id),
       frame_routing_id_(frame_routing_id),
       ukm_source_id_(ukm::kInvalidSourceIdObj),
       proxied_loader_receiver_(this),
@@ -214,23 +215,25 @@
       has_any_security_info_listeners_(
           WebRequestEventRouter::Get(factory_->browser_context_)
               ->HasAnySecurityInfoListener(factory_->browser_context_)) {
-  TRACE_EVENT("extensions",
-              "WebRequestProxyingURLLoaderFactory::InProgressRequest::"
-              "InProgressRequest",
-              perfetto::Flow::ProcessScoped(
-                  request_id_, kWebRequestProxyingURLLoaderFactoryScope),
-              "url", request.url.spec());
+  TRACE_EVENT(
+      "extensions",
+      "WebRequestProxyingURLLoaderFactory::InProgressRequest::"
+      "InProgressRequest",
+      perfetto::Flow::ProcessScoped(profile_request_id_,
+                                    kWebRequestProxyingURLLoaderFactoryScope),
+      "url", request.url.spec());
 }
 
 WebRequestProxyingURLLoaderFactory::InProgressRequest::~InProgressRequest() {
   DCHECK_NE(state_, State::kInvalid);
 
-  TRACE_EVENT("extensions",
-              "WebRequestProxyingURLLoaderFactory::InProgressRequest::"
-              "~InProgressRequest",
-              perfetto::TerminatingFlow::ProcessScoped(
-                  request_id_, kWebRequestProxyingURLLoaderFactoryScope),
-              "state", state_);
+  TRACE_EVENT(
+      "extensions",
+      "WebRequestProxyingURLLoaderFactory::InProgressRequest::"
+      "~InProgressRequest",
Loading diff…

Original Bug Report

reported by [email protected]

Cross-origin secret leak via request ID collisions in WebRequest API

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic vulnerability in WebRequestProxyingURLLoaderFactory allows a compromised renderer to use duplicate request IDs to cause map collisions in the browser. This can trick extensions into injecting sensitive cross-origin secrets, such as authentication tokens, into attacker-controlled network requests.

Affected files:

  • extensions/browser/api/web_request/web_request_proxying_url_loader_factory.cc
  • extensions/browser/api/web_request/web_request_api.cc

Estimated timestamp from git blame: 2018-06-13

Summary

A logic vulnerability in WebRequestProxyingURLLoaderFactory allows a compromised renderer process to perform a confused-deputy attack by providing duplicate request_id values for network requests. Because the browser process lacks uniqueness validation for these IDs in release builds, it can be tricked into routing network events and extension callbacks intended for a victim origin to a request destined for an attacker-controlled origin.

Root Cause Analysis

In extensions/browser/api/web_request/web_request_proxying_url_loader_factory.cc, the CreateLoaderAndStart method accepts a request_id parameter directly from the renderer via Mojo. This ID is used as a key in the network_request_id_to_web_request_id_ map. The code uses emplace(), which fails silently if the key already exists:

// extensions/browser/api/web_request/web_request_proxying_url_loader_factory.cc:1578
network_request_id_to_web_request_id_.emplace(request_id, web_request_id);

Similarly, in extensions/browser/api/web_request/web_request_api.cc, WebRequestAPI::ProxySet::AssociateProxyWithRequestId uses the ID as a map key, guarded only by a DCHECK which is absent in release builds:

// extensions/browser/api/web_request/web_request_api.cc:289
auto result = request_id_to_proxy_map_.emplace(id, proxy);
DCHECK(result.second) << "Unexpected request ID collision.";

When a collision occurs, the browser continues to use the stale mapping associated with the first request. When the network service subsequently triggers callbacks (like OnLoaderCreated or OnAuthRequired) using that request_id, the factory retrieves the metadata for the first request (e.g., a victim origin) but applies the action to the second request (e.g., the attacker’s origin).

Potential Attack Scenario

  1. Setup: An attacker compromises a renderer process.
  2. Collision: The renderer initiates two requests using the same request_id = X. Request A is for https://victim.corp/ and Request B is for https://attacker.com/.
  3. Hijacking: The browser maps X to Request A’s internal state. When the network service creates the loader for Request B, the browser incorrectly routes the TrustedHeaderClient receiver to Request A’s InProgressRequest object.
  4. Confused Deputy: Request B’s load triggers an OnBeforeSendHeaders event. Request A handles this event, sees its own metadata (victim.corp), and notifies extensions. An extension injects a secret (e.g., a Bearer token) into the headers.
  5. Exfiltration: Request A returns the modified headers (now containing the victim’s token) to the network stack to be used for Request B. The secret is sent to the attacker’s server.

Impact

This issue potentially allows a compromised renderer to bypass Site Isolation guarantees by exfiltrating sensitive credentials, cookies, or authentication headers managed by extensions. It affects any extension that relies on the webRequest API to modify headers or provide credentials based on URL filters.

Suggested Fix

The browser process should validate the uniqueness of the renderer-provided request_id. If emplace or insert fails because the ID is already present in the map, the browser should treat this as a compromised renderer and terminate the process using mojo::ReportBadMessage or a similar mechanism.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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