Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Extensions
DescriptionInsufficient policy enforcement in Extensions
ComponentExtensions
Bug ClassLogic Error
Tracker496279876
Fix commit2619b946b249 (chromium/src) +56/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
IN_PROC_BROWSER_TEST_F
chrome/browser/extensions/api/web_request/web_request_apitest.cc
modified

Files Changed

  • chrome/browser/extensions/api/web_request/web_request_apitest.cc
  • extensions/browser/api/web_request/extension_web_request_event_router.cc
  • extensions/browser/api/web_request/web_request_api.cc
From 2619b946b249d21cc1f2f1e08edcec3ecb8fdc4e Mon Sep 17 00:00:00 2001
From: Andrea Orru <andreaorru@chromium.org>
Date: Fri, 27 Mar 2026 10:40:07 -0700
Subject: [PATCH] [Extensions] Enforce webRequestBlocking for non-webview requests

This issue was discovered by Flapjack (go/flapjack).

A compromised extension renderer could bypass the webRequestBlocking
permission check by registering a listener for a general browser traffic
event while supplying a non-zero web_view_instance_id.

The browser process would check for the "webview" permission but skip
the blocking permission check. Later, the listener would match general
browser requests because it lacked a reverse filter to reject
webview-scoped listeners for non-webview requests.

Fixed: 496279876
Change-Id: Iffa1292ec9c9aa718de4dbf00514b289d324971c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7702712
Commit-Queue: Andrea Orru <andreaorru@chromium.org>
Reviewed-by: Devlin Cronin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1606344}
---

diff --git a/chrome/browser/extensions/api/web_request/web_request_apitest.cc b/chrome/browser/extensions/api/web_request/web_request_apitest.cc
index 5ca6b54..1e68232 100644
--- a/chrome/browser/extensions/api/web_request/web_request_apitest.cc
+++ b/chrome/browser/extensions/api/web_request/web_request_apitest.cc
@@ -93,6 +93,7 @@
 #include "content/public/test/web_transport_simple_test_server.h"
 #include "extensions/browser/api/web_request/extension_web_request_event_router.h"
 #include "extensions/browser/api/web_request/web_request_api.h"
+#include "extensions/browser/api_test_utils.h"
 #include "extensions/browser/background_script_executor.h"
 #include "extensions/browser/blocked_action_type.h"
 #include "extensions/browser/event_router.h"
@@ -8695,6 +8696,48 @@
                       GetWebSocketServer().GetURL("/echo-with-no-extension"));
 }
 
+// Regression test for https://crbug.com/496279876.
+// Tests that an extension renderer cannot bypass the webRequestBlocking
+// permission check by registering a listener for a general browser traffic
+// event while supplying a non-zero `web_view_instance_id`.
+IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, InvalidWebViewInstanceId) {
+  // Setup an extension that has <webview> privileges but explicitly lacks
+  // the "webRequestBlocking" permission to simulate the bug's scenario.
+  scoped_refptr<const Extension> extension =
+      ExtensionBuilder("webview_extension")
+          .AddAPIPermissions({"webview", "webRequest"})
+          .AddHostPermission("<all_urls>")
+          .Build();
+  ASSERT_TRUE(extension);
+
+  auto function =
+      base::MakeRefCounted<WebRequestInternalAddEventListenerFunction>();
+  function->set_extension(extension.get());
+  function->set_has_callback(true);
+
+  // Arguments for webRequestInternal.addEventListener:
+  //   0. `callback`: not used here.
+  //   1. `filter`: the RequestFilter dictionary.
+  //   2. `extraInfoSpec`: use "blocking" option to require the permission.
+  //   3. `eventName`: the base event name.
+  //   4. `subEventName`: uniquely identifies the listener.
+  //   5. `webViewInstanceId`: a non-zero integer representing a webview.
+  std::string args = R"([
+    {},
+    {"urls": ["<all_urls>"]},
+    ["blocking"],
+    "webRequest.onBeforeRequest",
+    "webRequest.onBeforeRequest/s1",
+    1
+  ])";
+  std::string error = api_test_utils::RunFunctionAndReturnError(
+      function.get(), args, profile());
+
+  // Supplying a non-zero `web_view_instance_id` for a general webRequest event
+  // should be immediately caught by the prefix validation.
+  EXPECT_EQ("Invalid event name for webview.", error);
+}
+
 // This test verifies that various types of network requests (defined in
 // chrome/test/data/webview/request_interception_coverage_guest.js) are
 // correctly intercepted by the extensions::WebRequestAPI. The same test logic
diff --git a/extensions/browser/api/web_request/extension_web_request_event_router.cc b/extensions/browser/api/web_request/extension_web_request_event_router.cc
index e0298d2..b11485ab 100644
--- a/extensions/browser/api/web_request/extension_web_request_event_router.cc
+++ b/extensions/browser/api/web_request/extension_web_request_event_router.cc
@@ -2733,6 +2733,8 @@
         listener.id.web_view_instance_id != request.web_view_instance_id) {
       return false;
     }
+  } else if (listener.id.web_view_instance_id != 0) {
+    return false;
   }
 
   // Filter requests from other extensions / apps. This does not work for
diff --git a/extensions/browser/api/web_request/web_request_api.cc b/extensions/browser/api/web_request/web_request_api.cc
index 28ebaf1d..4902e39 100644
--- a/extensions/browser/api/web_request/web_request_api.cc
+++ b/extensions/browser/api/web_request/web_request_api.cc
@@ -501,6 +501,13 @@
                                      "Missing webview permission.");
       return;
     }
+
+    if (!event_name.starts_with("webViewInternal.")) {
+      AddMessageToConsoleForListener(details,
+                                     blink::mojom::ConsoleMessageLevel::kError,
+                                     "Invalid event name for webview.");
+      return;
+    }
   } else {
     auto has_blocking_permission = [&extension, &event_name]() {
       DCHECK(extension);
@@ -1230,6 +1237,10 @@
                          mojom::APIPermissionID::kWebView)) {
       return RespondNow(Error("Missing webview permission."));
     }
+
+    if (!event_name.starts_with("webViewInternal.")) {
+      return RespondNow(Error("Invalid event name for webview."));
+    }
   } else {
     auto has_blocking_permission = [&extension, &event_name]() {
       if (extension->permissions_data()->HasAPIPermission(
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/extensions/api/web_request/web_request_apitest.cc b/chrome/browser/extensions/api/web_request/web_request_apitest.cc
index 5ca6b54..1e68232 100644
--- a/chrome/browser/extensions/api/web_request/web_request_apitest.cc
+++ b/chrome/browser/extensions/api/web_request/web_request_apitest.cc
@@ -93,6 +93,7 @@
 #include "content/public/test/web_transport_simple_test_server.h"
 #include "extensions/browser/api/web_request/extension_web_request_event_router.h"
 #include "extensions/browser/api/web_request/web_request_api.h"
+#include "extensions/browser/api_test_utils.h"
 #include "extensions/browser/background_script_executor.h"
 #include "extensions/browser/blocked_action_type.h"
 #include "extensions/browser/event_router.h"
@@ -8695,6 +8696,48 @@
                       GetWebSocketServer().GetURL("/echo-with-no-extension"));
 }
 
+// Regression test for https://crbug.com/496279876.
+// Tests that an extension renderer cannot bypass the webRequestBlocking
+// permission check by registering a listener for a general browser traffic
+// event while supplying a non-zero `web_view_instance_id`.
+IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, InvalidWebViewInstanceId) {
+  // Setup an extension that has <webview> privileges but explicitly lacks
+  // the "webRequestBlocking" permission to simulate the bug's scenario.
+  scoped_refptr<const Extension> extension =
+      ExtensionBuilder("webview_extension")
+          .AddAPIPermissions({"webview", "webRequest"})
+          .AddHostPermission("<all_urls>")
+          .Build();
+  ASSERT_TRUE(extension);
+
+  auto function =
+      base::MakeRefCounted<WebRequestInternalAddEventListenerFunction>();
+  function->set_extension(extension.get());
+  function->set_has_callback(true);
+
+  // Arguments for webRequestInternal.addEventListener:
+  //   0. `callback`: not used here.
+  //   1. `filter`: the RequestFilter dictionary.
+  //   2. `extraInfoSpec`: use "blocking" option to require the permission.
+  //   3. `eventName`: the base event name.
+  //   4. `subEventName`: uniquely identifies the listener.
+  //   5. `webViewInstanceId`: a non-zero integer representing a webview.
+  std::string args = R"([
+    {},
+    {"urls": ["<all_urls>"]},
+    ["blocking"],
+    "webRequest.onBeforeRequest",
+    "webRequest.onBeforeRequest/s1",
+    1
+  ])";
+  std::string error = api_test_utils::RunFunctionAndReturnError(
+      function.get(), args, profile());
+
+  // Supplying a non-zero `web_view_instance_id` for a general webRequest event
+  // should be immediately caught by the prefix validation.
+  EXPECT_EQ("Invalid event name for webview.", error);
+}
+
 // This test verifies that various types of network requests (defined in
 // chrome/test/data/webview/request_interception_coverage_guest.js) are
 // correctly intercepted by the extensions::WebRequestAPI. The same test logic
Loading diff…

Original Bug Report

reported by vm...@google.com

Permission bypass in WebRequest API allows compromised renderer to intercept general traffic

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

Overview: A logic flaw allows a compromised extension renderer to bypass the webRequestBlocking permission requirement. By forging an IPC message with a non-zero web_view_instance_id, an attacker can register a blocking listener for general browser traffic without the necessary permissions.

Affected files:

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

Estimated timestamp from git blame: 2025-03-05

Description

A vulnerability exists in the Chrome Extensions Web Request API where a compromised extension renderer can bypass the webRequestBlocking permission check. This is achieved by exploiting an asymmetric check in ExtensionWebRequestEventRouter::ListenerMatchesRequest and a conditional permission check in WebRequestInternalAddEventListenerFunction::Run.

When registering a listener via the internal webRequestInternal.addEventListener API, the browser process checks if web_view_instance_id is non-zero. If it is, the browser verifies the extension has the webview permission but skips the webRequestBlocking check (which is normally required for blocking listeners). A compromised renderer can send a forged IPC to register a listener for a general event (e.g., webRequest.onBeforeRequest) while supplying a non-zero web_view_instance_id and the BLOCKING flag.

This places the listener into the ordinary browser traffic bucket. Later, when a general browser request occurs (request.is_web_view == false), ListenerMatchesRequest correctly skips the webview ID check but lacks a reverse filter to reject listeners that possess a non-zero web_view_instance_id. As a result, the listener matches the request, allowing the extension to block or modify general browser traffic without the webRequestBlocking permission.

Potential Steps to Reproduce

Note: Our setup does not currently have the ability to run code to verify this, but the following steps describe how an attacker would theoretically trigger the vulnerability:

  1. An attacker compromises the renderer process of an extension or Platform App that has the webview permission and host permissions, but lacks the webRequestBlocking permission.
  2. From the compromised renderer, the attacker sends a forged webRequestInternal.addEventListener IPC message to the browser process.
  3. The IPC payload specifies event_name as "webRequest.onBeforeRequest", sets web_view_instance_id to a non-zero value (e.g., 1), and includes the BLOCKING flag in the extra_info_spec.
  4. In the browser process, WebRequestInternalAddEventListenerFunction::Run evaluates if (web_view_instance_id). Since it is non-zero, it checks for the webview permission and skips the else block containing the webRequestBlocking permission check.
  5. The listener is successfully added to the active_listeners["webRequest.onBeforeRequest"] bucket.
  6. When the user navigates to a URL matching the extension’s host permissions in a regular browser tab, a WebRequestInfo object is created with is_web_view set to false.
  7. ListenerMatchesRequest evaluates the listener. Because request.is_web_view is false, the webview ID matching block is skipped.
  8. The listener successfully matches the request, and the compromised extension is permitted to intercept, modify, or block the request, despite lacking the required permissions.

Suggested Fix

  1. Registration Check: In WebRequestInternalAddEventListenerFunction::Run, enforce that if web_view_instance_id is non-zero, the event_name must start with the webViewInternal. prefix (e.g., kWebViewEventPrefix).
  2. Match Check: In ExtensionWebRequestEventRouter::ListenerMatchesRequest, add a reverse filter to reject webview-scoped listeners for non-webview requests:
if (!request.is_web_view && listener.id.web_view_instance_id != 0) {
  return false;
}

Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8


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