CVE-2026-7952
Overview
Background
- `webRequest` / `webRequestBlocking`
- the extension API for observing/modifying network requests; the blocking (synchronous, request-altering) variant requires the
webRequestBlockingpermission. - `web_view_instance_id`
- a listener field identifying which
<webview>guest the listener is scoped to;0means a non-webview (general browser traffic) listener. - `webViewInternal.` events
- the internal event-name namespace used for webview-scoped web-request events, distinct from general
webRequest.*events. - Listener matching
- when a request arrives, the event router tests each registered listener’s filters (including
web_view_instance_id) to decide whether to deliver and let it block the request.
Root Cause Analysis
Two cooperating defects existed. At registration (web_request_api.cc), when a listener supplied a non-zero web_view_instance_id the code checked only the kWebView permission and then skipped straight past the permission logic, never reaching the else branch that enforces webRequestBlocking for blocking listeners — so a compromised extension renderer could claim to be a webview listener to dodge the blocking-permission check. At matching (extension_web_request_event_router.cc), the listener-filtering logic only rejected a webview-scoped listener when the request was itself a webview request with a different id; it had no reverse filter, so a listener carrying a non-zero web_view_instance_id would still match general (non-webview) browser requests. Combined, a listener registered for a general event (e.g. webRequest.onBeforeRequest) with web_view_instance_id = 1 escaped the webRequestBlocking check yet still matched and could block ordinary browser traffic.
The fix tightens both ends: registration now additionally requires the event name to starts_with("webViewInternal.") in the webview branch (rejecting general events supplied with a webview id via console error / Error("Invalid event name for webview.")), and matching adds else if (listener.id.web_view_instance_id != 0) return false; so a webview-scoped listener never matches a non-webview request.
web_view_instance_id as “this is a webview listener” let a renderer take the webview-only permission path (skipping webRequestBlocking) while the listener still matched general traffic; the fix rejects non-webViewInternal. events on registration and rejects webview-scoped listeners against non-webview requests on matching.Attack Path
- Compromised extension renderer
The attacker controls an extension renderer that has the
webviewpermission but lackswebRequestBlocking. - Register a mislabeled blocking listener
Via
webRequestInternal.addEventListener, it registers a listener for a general event (webRequest.onBeforeRequest) with the"blocking"extraInfoSpec and a non-zeroweb_view_instance_id(e.g.1), as in the regression test. - Skip the blocking-permission check
Pre-patch, the browser sees the non-zero webview id, checks only the
kWebViewpermission, and never evaluates thewebRequestBlockingrequirement, so the blocking listener is accepted. - Intercept general traffic Because matching lacked the reverse filter, the webview-scoped listener matches ordinary browser requests, letting the extension synchronously observe/block general traffic without the required permission, the policy bypass the advisory describes.
Impact Assessment
webRequestBlocking permission enforcement and register a blocking listener that intercepts general browser requests; the advisory rates this medium and classifies it as insufficient policy enforcement in Extensions. The precondition is a compromised extension renderer holding the webview permission (the advisory credits internal discovery via “Flapjack”). The advisory does not report in-the-wild exploitation, and the impact is a permission/policy bypass rather than demonstrated memory corruption.Changed Functions
| Function | Change | Notes |
|---|---|---|
IN_PROC_BROWSER_TEST_Fchrome/browser/extensions/api/web_request/web_request_apitest.cc |
modified |
Files Changed
chrome/browser/extensions/api/web_request/web_request_apitest.ccextensions/browser/api/web_request/extension_web_request_event_router.ccextensions/browser/api/web_request/web_request_api.cc
Audit Directions
- Permission checks keyed on a mode flagAudit APIs where supplying an identifier/flag (like
web_view_instance_id) switches to a different, weaker permission branch; ensure the chosen branch is consistent with the requested capability. - Missing reverse filters in matchingLook for filter logic that constrains scope in one direction only (request-with-id vs listener-with-id) and verify both the webview and non-webview cases are mutually exclusive.
- Event-name namespace enforcementReview internal/privileged event namespaces (
webViewInternal., other*Internal.events) to confirm the browser validates the event name against the claimed scope rather than trusting the renderer.
Patch
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(
Regression Test / PoC
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
Original Bug Report
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.ccextensions/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:
- An attacker compromises the renderer process of an extension or Platform App that has the
webviewpermission and host permissions, but lacks thewebRequestBlockingpermission. - From the compromised renderer, the attacker sends a forged
webRequestInternal.addEventListenerIPC message to the browser process. - The IPC payload specifies
event_nameas"webRequest.onBeforeRequest", setsweb_view_instance_idto a non-zero value (e.g.,1), and includes theBLOCKINGflag in theextra_info_spec. - In the browser process,
WebRequestInternalAddEventListenerFunction::Runevaluatesif (web_view_instance_id). Since it is non-zero, it checks for thewebviewpermission and skips theelseblock containing thewebRequestBlockingpermission check. - The listener is successfully added to the
active_listeners["webRequest.onBeforeRequest"]bucket. - When the user navigates to a URL matching the extension’s host permissions in a regular browser tab, a
WebRequestInfoobject is created withis_web_viewset tofalse. ListenerMatchesRequestevaluates the listener. Becauserequest.is_web_viewisfalse, the webview ID matching block is skipped.- 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
- Registration Check: In
WebRequestInternalAddEventListenerFunction::Run, enforce that ifweb_view_instance_idis non-zero, theevent_namemust start with thewebViewInternal.prefix (e.g.,kWebViewEventPrefix). - 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.