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
Tracker513321171
Fix commit572cfa5066ce (chromium/src) +53/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-08

Changed Functions

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

Files Changed

  • chrome/browser/extensions/extension_security_exploit_browsertest.cc
  • extensions/browser/api/web_request/web_request_api.cc
  • extensions/browser/bad_message.h
  • tools/metrics/histograms/metadata/stability/enums.xml
From 572cfa5066cece423fd070de5d61395bf947630b Mon Sep 17 00:00:00 2001
From: Andrea Orru <[email protected]>
Date: Tue, 26 May 2026 11:29:01 -0700
Subject: [PATCH] [Extensions] Fix Site Isolation bypass in WebRequest API via EventRouter

When kWebRequestPersistFilteredEventsViaEventRouter is enabled, active
webRequest listeners are registered via the mojom::EventRouter
interface. EventRouter::AddFilteredListenerForMainThread permits
registrations from renderers that have executed a content script for the
extension. However, WebRequestAPI::OnListenerAdded does not verify
whether the registering process was a privileged extension process
rather than a content script web process before establishing active
listeners that receive sensitive cross-origin network data.

This change adds a process authorization check in
WebRequestAPI::OnListenerAdded using ProcessMap::Contains. If an
unauthorized process attempts to register a webRequest listener, the
registration is rejected and the renderer process is terminated with the
new bad message reason WRA_INVALID_EXTENSION_ID_FOR_PROCESS.

Bug: 513321171
Change-Id: Iad4aca0a303cc64cec5f280bc12a21c3b03cc799
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7867503
Reviewed-by: Tim <[email protected]>
Reviewed-by: Kelvin Jiang <[email protected]>
Reviewed-by: Luc Nguyen <[email protected]>
Commit-Queue: Andrea Orru <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1636336}
---

diff --git a/chrome/browser/extensions/extension_security_exploit_browsertest.cc b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
index 1a3e243..5015d3de 100644
--- a/chrome/browser/extensions/extension_security_exploit_browsertest.cc
+++ b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
@@ -11,6 +11,7 @@
 #include "base/memory/weak_ptr.h"
 #include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
+#include "base/test/values_test_util.h"
 #include "base/values.h"
 #include "build/build_config.h"
 #include "chrome/browser/chrome_content_browser_client.h"
@@ -1807,4 +1808,39 @@
       bad_message_observer.WaitForBadMessage());
 }
 
+// Tests that a web page process that ran a content script cannot register an
+// active webRequest listener for that extension.
+IN_PROC_BROWSER_TEST_F(
+    EventRouterExploitTest,
+    AddFilteredListenerForMainThread_WebRequestFromContentScriptProcess) {
+  // Navigate to a web page where active_extension() has host permissions.
+  GURL test_page_url =
+      embedded_test_server()->GetURL("foo.com", "/title1.html");
+  auto* web_contents = GetActiveWebContents();
+  EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));
+
+  // Execute a content script so ScriptInjectionTracker authorizes this process
+  // for main thread listeners.
+  ASSERT_TRUE(ExecuteProgrammaticContentScript(
+      web_contents, active_extension_id(), "document.body.bgColor = 'red';"));
+
+  content::RenderProcessHost* main_frame_process =
+      web_contents->GetPrimaryMainFrame()->GetProcess();
+  mojo::AssociatedRemote<mojom::EventRouter> event_router =
+      BindEventRouterForProcess(main_frame_process);
+
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
+
+  base::DictValue filter =
+      base::test::ParseJsonDict(R"({"urls": ["http://*/*"]})");
+
+  event_router->AddFilteredListenerForMainThread(
+      mojom::EventListenerOwner::NewExtensionId(active_extension_id()),
+      "webRequest.onBeforeRequest", std::move(filter),
+      /*add_lazy_listener=*/false);
+
+  EXPECT_EQ(bad_message::WRA_INVALID_EXTENSION_ID_FOR_PROCESS,
+            kill_waiter.Wait());
+}
+
 }  // namespace extensions
diff --git a/extensions/browser/api/web_request/web_request_api.cc b/extensions/browser/api/web_request/web_request_api.cc
index a0ad3f59..02189d14 100644
--- a/extensions/browser/api/web_request/web_request_api.cc
+++ b/extensions/browser/api/web_request/web_request_api.cc
@@ -41,6 +41,7 @@
 #include "extensions/browser/api/web_request/web_request_proxying_url_loader_factory.h"
 #include "extensions/browser/api/web_request/web_request_proxying_websocket.h"
 #include "extensions/browser/api/web_request/web_request_proxying_webtransport.h"
+#include "extensions/browser/bad_message.h"
 #include "extensions/browser/browser_frame_context_data.h"
 #include "extensions/browser/browser_process_context_data.h"
 #include "extensions/browser/event_router.h"
@@ -467,6 +468,20 @@
   std::string sub_event_name = details.event_name;
   auto* process = content::RenderProcessHost::FromID(details.render_process_id);
 
+  // Active webRequest listeners owned by an extension must only be registered
+  // by processes authorized to host that extension.
+  // `AddFilteredListenerForMainThread` also accepts registrations from web
+  // processes that have executed content or user scripts for the extension, so
+  // enforce the stronger `ProcessMap` check here. Lazy listeners are exempt
+  // since they are not bound to a specific process. See crbug.com/513321171.
+  if (extension && !details.is_lazy && process &&
+      !ProcessMap::Get(details.browser_context)
+           ->Contains(extension->id(), process->GetID())) {
+    bad_message::ReceivedBadMessage(
+        process, bad_message::WRA_INVALID_EXTENSION_ID_FOR_PROCESS);
+    return;
+  }
+
   if (extra_info_spec & ExtraInfoSpec::SECURITY_INFO) {
     // Security info should not be available in Chrome Apps and
     // non-controlled frame, non-extension contexts.
diff --git a/extensions/browser/bad_message.h b/extensions/browser/bad_message.h
index 7e08ece..276ff421 100644
--- a/extensions/browser/bad_message.h
+++ b/extensions/browser/bad_message.h
@@ -76,6 +76,7 @@
   CEFH_INVALID_EXTENSION_ID_FOR_SCRIPT_INJECT_REQUEST = 36,
   SWH_INVALID_SERVICE_WORKER_SCOPE = 37,
   EMF_INVALID_MESSAGE_FROM_SANDBOXED_PROCESS = 38,
+  WRA_INVALID_EXTENSION_ID_FOR_PROCESS = 39,
   // Please add new elements here. The naming convention is abbreviated class
   // name (e.g. ExtensionHost becomes EH) plus a unique description of the
   // reason. After making changes, you MUST update histograms.xml by running:
diff --git a/tools/metrics/histograms/metadata/stability/enums.xml b/tools/metrics/histograms/metadata/stability/enums.xml
index c1e842a..6a78d773 100644
--- a/tools/metrics/histograms/metadata/stability/enums.xml
+++ b/tools/metrics/histograms/metadata/stability/enums.xml
@@ -567,6 +567,7 @@
   <int value="36" label="CEFH_INVALID_EXTENSION_ID_FOR_SCRIPT_INJECT_REQUEST"/>
   <int value="37" label="SWH_INVALID_SERVICE_WORKER_SCOPE"/>
   <int value="38" label="EMF_INVALID_MESSAGE_FROM_SANDBOXED_PROCESS"/>
+  <int value="39" label="WRA_INVALID_EXTENSION_ID_FOR_PROCESS"/>
 </enum>
 
 <enum name="BadMessageReasonGuestView">
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/extensions/extension_security_exploit_browsertest.cc b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
index 1a3e243..5015d3de 100644
--- a/chrome/browser/extensions/extension_security_exploit_browsertest.cc
+++ b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
@@ -11,6 +11,7 @@
 #include "base/memory/weak_ptr.h"
 #include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
+#include "base/test/values_test_util.h"
 #include "base/values.h"
 #include "build/build_config.h"
 #include "chrome/browser/chrome_content_browser_client.h"
@@ -1807,4 +1808,39 @@
       bad_message_observer.WaitForBadMessage());
 }
 
+// Tests that a web page process that ran a content script cannot register an
+// active webRequest listener for that extension.
+IN_PROC_BROWSER_TEST_F(
+    EventRouterExploitTest,
+    AddFilteredListenerForMainThread_WebRequestFromContentScriptProcess) {
+  // Navigate to a web page where active_extension() has host permissions.
+  GURL test_page_url =
+      embedded_test_server()->GetURL("foo.com", "/title1.html");
+  auto* web_contents = GetActiveWebContents();
+  EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));
+
+  // Execute a content script so ScriptInjectionTracker authorizes this process
+  // for main thread listeners.
+  ASSERT_TRUE(ExecuteProgrammaticContentScript(
+      web_contents, active_extension_id(), "document.body.bgColor = 'red';"));
+
+  content::RenderProcessHost* main_frame_process =
+      web_contents->GetPrimaryMainFrame()->GetProcess();
+  mojo::AssociatedRemote<mojom::EventRouter> event_router =
+      BindEventRouterForProcess(main_frame_process);
+
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
+
+  base::DictValue filter =
+      base::test::ParseJsonDict(R"({"urls": ["http://*/*"]})");
+
+  event_router->AddFilteredListenerForMainThread(
+      mojom::EventListenerOwner::NewExtensionId(active_extension_id()),
+      "webRequest.onBeforeRequest", std::move(filter),
+      /*add_lazy_listener=*/false);
+
+  EXPECT_EQ(bad_message::WRA_INVALID_EXTENSION_ID_FOR_PROCESS,
+            kill_waiter.Wait());
+}
+
 }  // namespace extensions
Loading diff…

Original Bug Report

reported by [email protected]

Site Isolation bypass via unauthorized webRequest listener registration

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

Overview: A compromised renderer process can potentially bypass Site Isolation by registering as a webRequest listener for an extension it has run a content script for. This allow an attacker to intercept sensitive cross-origin network data, including URLs, cookies, and POST bodies, from all other browser tabs.

Affected files:

  • extensions/browser/api/web_request/web_request_api.cc
  • extensions/browser/event_router.cc
  • extensions/browser/api/web_request/extension_web_request_event_router.cc
  • chrome/browser/extensions/chrome_content_browser_client_extensions_part_bindings.cc
  • extensions/common/extension_features.cc
  • extensions/browser/api/web_request/web_request_event_details.cc

Estimated timestamp from git blame: 2026-02-25

Potential Vulnerability Summary

A logic flaw in the Chromium extension event system potentially allows a compromised sandboxed renderer process to obtain cross-origin network request data. By exploiting the mojom::EventRouter Mojo interface, a renderer that has executed a content script for an extension can register itself as an active webRequest listener on behalf of that extension. Due to insufficient validation in the browser process, sensitive network data for all origins matching the extension’s permissions is delivered directly to the compromised renderer, bypassing Site Isolation boundaries.

Potential Root Cause Analysis

The issue arises when the kWebRequestPersistFilteredEventsViaEventRouter feature is enabled (which is the default). The vulnerability stems from the following sequence in the browser process:

  1. Permissive Registration: The EventRouter::AddFilteredListenerForMainThread method (at extensions/browser/event_router.cc:615) allows a process to register active listeners for an extension if it has run a content script for that extension (IsProcessAuthorizedForMainThreadExtensionListener). This is intended for events content scripts can legitimately receive, but it is not restricted for the sensitive webRequest API.
  2. Lack of Process Type Verification: When a listener is added, WebRequestAPI::OnListenerAdded (at extensions/browser/api/web_request/web_request_api.cc:415) is notified. It validates the extension’s permissions but fails to verify if the registering process is a privileged extension context (e.g., a background page) rather than a non-privileged web renderer hosting a content script.
  3. Security Gate Bypass during Dispatch: Active webRequest listeners are dispatched via a bespoke path using EventRouter::DispatchEventToSender (at extensions/browser/api/web_request/extension_web_request_event_router.cc:1796). This path bypasses the standard EventRouter::DispatchEventToProcess logic, which contains essential security checks such as CheckFeatureAvailability and context-type verification. These checks would normally prevent webRequest data (which contains cross-origin information) from being sent to a web page context.

Potential Impact

This is a high-severity Site Isolation bypass. An attacker with RCE in a renderer can impersonate a widely-used extension (like an ad-blocker) to monitor global browsing activity and steal sensitive information, including authentication cookies, authorization headers, and POST request data from other origins.

Suggested Potential Steps to Reproduce

  1. Identify an installed extension with broad host permissions (e.g., <all_urls>) that injects content scripts.
  2. Navigate a compromised renderer to a page where the extension’s content script executes to satisfy the ScriptInjectionTracker authorization check.
  3. From the compromised renderer, use the extensions.mojom.EventRouter Mojo interface (bound in chrome/browser/extensions/chrome_content_browser_client_extensions_part_bindings.cc:74) to call AddFilteredListenerForMainThread.
  4. Set listener_owner to the target extension ID, event_name to webRequest.onBeforeRequest, and include filters for all URLs with extraInfo flags (e.g., requestBody, requestHeaders).
  5. The renderer will then receive webRequest events via mojom::EventDispatcher::DispatchEvent for all matching network requests made by the browser.

Suggested Fix

WebRequestAPI::OnListenerAdded (or the underlying WebRequestEventRouter::AddEventListener) should be updated to verify that the render_process_id belongs to a privileged extension process for the given extension_id (e.g., using ProcessMap::Contains) before allowing the registration of webRequest listeners, unless the context is a legitimate webView or controlledFrame with appropriate permissions.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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