CVE-2026-11308
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/extensions/extension_action_dispatcher.cc |
modified | |
forchrome/browser/extensions/extension_action_dispatcher.cc |
modified |
Files Changed
chrome/browser/extensions/api/extension_action/extension_action_apitest.ccchrome/browser/extensions/extension_action_dispatcher.ccchrome/browser/extensions/extension_action_dispatcher.h
Patch
From 68be55bcd89e9ee541bb13d36dabb28836b5ce3f Mon Sep 17 00:00:00 2001 From: Tim Judkins <[email protected]> Date: Tue, 28 Apr 2026 16:27:44 -0700 Subject: [PATCH] [Extensions] Pass UserGestureState explicitly for action events Previously both the onClicked and onUserSettingsChanged events in the action API both used the same helper to dispatch the events and in both cases a user_gesture was set. This CL changes it so that the two callers instead specify if a user gester should be set, as onUserSettingsChanged doesn't need one. Also adds to the related tests to validate the user gesture state, moves the related function into the anonymous namespace as it's only called from within this class and removes a <string> include that's not needed in the header anymore. Bug: 505945112 Change-Id: I16f98d7ecf8f22e50a12751f575fdcbdcd06dd37 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7797497 Reviewed-by: Devlin Cronin <[email protected]> Commit-Queue: Tim <[email protected]> Cr-Commit-Position: refs/heads/main@{#1622101} --- diff --git a/chrome/browser/extensions/api/extension_action/extension_action_apitest.cc b/chrome/browser/extensions/api/extension_action/extension_action_apitest.cc index f33042a6..37a4279e 100644 --- a/chrome/browser/extensions/api/extension_action/extension_action_apitest.cc +++ b/chrome/browser/extensions/api/extension_action/extension_action_apitest.cc @@ -526,6 +526,7 @@ chrome.test.assertTrue(!!tab); chrome.test.assertTrue(tab.id > 0); chrome.test.assertTrue(tab.index > -1); + chrome.test.assertTrue(chrome.test.isProcessingUserGesture()); chrome.test.notifyPass(); });)"; @@ -1855,7 +1856,8 @@ })"; constexpr char kWorker[] = R"(chrome.action.onUserSettingsChanged.addListener(change => { - chrome.test.sendMessage(JSON.stringify(change)); + const userGesture = chrome.test.isProcessingUserGesture(); + chrome.test.sendMessage(JSON.stringify({change, userGesture})); });)"; TestExtensionDir test_dir; @@ -1878,10 +1880,10 @@ return listener.message(); }; - EXPECT_EQ(R"({"isOnToolbar":true})", + EXPECT_EQ(R"({"change":{"isOnToolbar":true},"userGesture":false})", change_visibility_and_get_response(/*pinned_state=*/true)); - EXPECT_EQ(R"({"isOnToolbar":false})", + EXPECT_EQ(R"({"change":{"isOnToolbar":false},"userGesture":false})", change_visibility_and_get_response(/*pinned_state=*/false)); } diff --git a/chrome/browser/extensions/extension_action_dispatcher.cc b/chrome/browser/extensions/extension_action_dispatcher.cc index e7f9bdb..c34bb38 100644 --- a/chrome/browser/extensions/extension_action_dispatcher.cc +++ b/chrome/browser/extensions/extension_action_dispatcher.cc @@ -21,6 +21,29 @@ namespace extensions { +namespace { + +// Forwards events to the `context`'s event router. +void DispatchEventToExtension( + content::BrowserContext* context, + const ExtensionId& extension_id, + events::HistogramValue histogram_value, + const std::string& event_name, + base::ListValue event_args, + EventRouter::UserGestureState user_gesture_state) { + auto* event_router = EventRouter::Get(context); + if (!event_router) { + return; + } + + auto event = std::make_unique<Event>(histogram_value, event_name, + std::move(event_args), context); + event->user_gesture = user_gesture_state; + event_router->DispatchEventToExtension(extension_id, std::move(event)); +} + +} // namespace + static base::LazyInstance< BrowserContextKeyedAPIFactory<ExtensionActionDispatcher>>::DestructorAtExit g_extension_action_dispatcher_factory = LAZY_INSTANCE_INITIALIZER; @@ -96,7 +119,8 @@ DispatchEventToExtension(web_contents->GetBrowserContext(), extension_action.extension_id(), histogram_value, - event_name, std::move(args)); + event_name, std::move(args), + EventRouter::UserGestureState::kEnabled); } } @@ -137,22 +161,6 @@ return extension_prefs_; } -void ExtensionActionDispatcher::DispatchEventToExtension( - content::BrowserContext* context, - const ExtensionId& extension_id, - events::HistogramValue histogram_value, - const std::string& event_name, - base::ListValue event_args) { - if (!EventRouter::Get(context)) { - return; - } - - auto event = std::make_unique<Event>(histogram_value, event_name, - std::move(event_args), context); - event->user_gesture = EventRouter::UserGestureState::kEnabled; - EventRouter::Get(context)->DispatchEventToExtension(extension_id, - std::move(event)); -} void ExtensionActionDispatcher::Shutdown() { for (auto& observer : observers_) { @@ -172,7 +180,8 @@ args.Append(std::move(change)); DispatchEventToExtension(browser_context_, extension_id, events::ACTION_ON_USER_SETTINGS_CHANGED, - "action.onUserSettingsChanged", std::move(args)); + "action.onUserSettingsChanged", std::move(args), + EventRouter::UserGestureState::kNotEnabled); } } // namespace extensions diff --git a/chrome/browser/extensions/extension_action_dispatcher.h b/chrome/browser/extensions/extension_action_dispatcher.h index f20fd7d..bf5e651 100644 --- a/chrome/browser/extensions/extension_action_dispatcher.h +++ b/chrome/browser/extensions/extension_action_dispatcher.h @@ -5,8 +5,6 @@ #ifndef CHROME_BROWSER_EXTENSIONS_EXTENSION_ACTION_DISPATCHER_H_ #define CHROME_BROWSER_EXTENSIONS_EXTENSION_ACTION_DISPATCHER_H_ -#include <string> - #include "base/memory/raw_ptr.h" #include "base/observer_list.h" #include "base/scoped_observation.h" @@ -103,13 +101,6 @@ // Returns the associated extension prefs. ExtensionPrefs* GetExtensionPrefs(); - // The DispatchEvent methods forward events to the `context`'s event router. - void DispatchEventToExtension(content::BrowserContext* context, - const ExtensionId& extension_id, - events::HistogramValue histogram_value, - const std::string& event_name, - base::ListValue event_args); - // BrowserContextKeyedAPI implementation. void Shutdown() override; static const char* service_name() { return "ExtensionActionDispatcher"; }
Regression Test / PoC
diff --git a/chrome/browser/extensions/api/extension_action/extension_action_apitest.cc b/chrome/browser/extensions/api/extension_action/extension_action_apitest.cc
index f33042a6..37a4279e 100644
--- a/chrome/browser/extensions/api/extension_action/extension_action_apitest.cc
+++ b/chrome/browser/extensions/api/extension_action/extension_action_apitest.cc
@@ -526,6 +526,7 @@
chrome.test.assertTrue(!!tab);
chrome.test.assertTrue(tab.id > 0);
chrome.test.assertTrue(tab.index > -1);
+ chrome.test.assertTrue(chrome.test.isProcessingUserGesture());
chrome.test.notifyPass();
});)";
@@ -1855,7 +1856,8 @@
})";
constexpr char kWorker[] =
R"(chrome.action.onUserSettingsChanged.addListener(change => {
- chrome.test.sendMessage(JSON.stringify(change));
+ const userGesture = chrome.test.isProcessingUserGesture();
+ chrome.test.sendMessage(JSON.stringify({change, userGesture}));
});)";
TestExtensionDir test_dir;
@@ -1878,10 +1880,10 @@
return listener.message();
};
- EXPECT_EQ(R"({"isOnToolbar":true})",
+ EXPECT_EQ(R"({"change":{"isOnToolbar":true},"userGesture":false})",
change_visibility_and_get_response(/*pinned_state=*/true));
- EXPECT_EQ(R"({"isOnToolbar":false})",
+ EXPECT_EQ(R"({"change":{"isOnToolbar":false},"userGesture":false})",
change_visibility_and_get_response(/*pinned_state=*/false));
}
Original Bug Report
User gesture bypass via action.onUserSettingsChanged event
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: The action.onUserSettingsChanged extension event is dispatched with an unconditional synthetic user gesture. This event can be triggered non-interactively, such as when an enterprise policy modifies an extension’s pinned state. Malicious extensions can potentially abuse this synthetic gesture to bypass user interaction requirements for restricted APIs like chrome.permissions.request().
Affected files:
chrome/browser/extensions/extension_action_dispatcher.ccchrome/browser/ui/toolbar/toolbar_actions_model.cc
Estimated timestamp from git blame: 2026-01-25
Summary
When an extension’s pinned state on the toolbar changes, the browser dispatches the chrome.action.onUserSettingsChanged event to the extension. However, the internal method responsible for constructing this event unconditionally flags it as possessing a user gesture. Because this event can be triggered by non-interactive means (such as an enterprise administrator updating the ExtensionSettings policy to set toolbar_pin to "default_pinned"), a malicious extension can receive a synthetic user gesture without any actual user interaction.
This synthetic gesture propagates to the extension’s renderer process (including Manifest V3 Service Workers), potentially allowing the extension to invoke restricted APIs that normally require a user gesture, such as chrome.permissions.request() or chrome.sidePanel.open().
Root Cause
In chrome/browser/extensions/extension_action_dispatcher.cc, the OnActionPinnedStateChanged method is called when an extension’s pinned state changes (e.g., via ToolbarActionsModel::OnExtensionManagementSettingsChanged during a policy update).
OnActionPinnedStateChanged calls a helper method, DispatchEventToExtension, to construct and send the event. The vulnerability exists because DispatchEventToExtension unconditionally sets event->user_gesture = EventRouter::UserGestureState::kEnabled; at line 152:
void ExtensionActionDispatcher::DispatchEventToExtension(
content::BrowserContext* context,
const ExtensionId& extension_id,
events::HistogramValue histogram_value,
const std::string& event_name,
base::ListValue event_args) {
if (!EventRouter::Get(context)) {
return;
}
auto event = std::make_unique<Event>(histogram_value, event_name,
std::move(event_args), context);
event->user_gesture = EventRouter::UserGestureState::kEnabled; // Unconditional user gesture
EventRouter::Get(context)->DispatchEventToExtension(extension_id,
std::move(event));
}
While this helper is also used for action.onClicked (which does involve a user gesture), reusing it for onUserSettingsChanged incorrectly grants a gesture to automated state changes.
Potential Reproduction Steps
Note: These are theoretical steps based on code analysis.
- An attacker creates a Manifest V3 extension that listens to
chrome.action.onUserSettingsChanged. - Inside the listener callback, the extension calls
chrome.permissions.request({permissions: ['activeTab']}). - The extension is installed on a managed device.
- The enterprise administrator pushes an update to the
ExtensionSettingspolicy, settingtoolbar_pinto"default_pinned"for the installed extension. ToolbarActionsModel::OnExtensionManagementSettingsChangedprocesses the policy and triggers the event dispatch.- The extension receives the event with the synthetic user gesture, and the permissions request prompt appears unexpectedly without any user interaction.
Suggested Fix
Modify ExtensionActionDispatcher::DispatchEventToExtension to accept an additional boolean or enum parameter indicating whether a user gesture is present.
- When called from
DispatchExtensionActionClicked, passtrue(orkEnabled). - When called from
OnActionPinnedStateChanged, evaluate the context. If the change was initiated by an automated policy update, passfalse(orkNotEnabled). Alternatively, as pinning/unpinning from the menu might not warrant bypassing gesture checks for restricted APIs anyway, consider always passingfalseforonUserSettingsChanged.
Evaluated with Chrome root at commit: 3acbde3302da0cb19488c22c0eb007c791207b4b
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. And please feel free to reach out to me directly if you have concerns or feedback on the project.