Chrome · Extensions › Extensions
CVE-2026-113321
UAF in Extensions (Extensions)
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/extensions/menu_manager.cc |
modified | |
ContextMenusTabScrubbingTestchrome/browser/extensions/api/context_menus/context_menu_apitest.cc |
modified | |
forchrome/browser/extensions/api/context_menus/context_menu_apitest.cc |
modified | |
switchchrome/browser/extensions/api/context_menus/context_menu_apitest.cc |
modified | |
ifchrome/browser/extensions/api/context_menus/context_menu_apitest.cc |
modified |
Files Changed
chrome/browser/extensions/menu_manager.ccchrome/browser/extensions/api/context_menus/context_menu_apitest.cc
Patch
From f571bdc350d6f8555367db9ceb62eb93c89bdb8b Mon Sep 17 00:00:00 2001
From: Tim <tjudkins@chromium.org>
Date: Thu, 17 Sep 2026 15:46:58 -0700
Subject: [PATCH] [Extensions] Apply tab scrubbing in context menu command execution
When dispatching `chrome.contextMenus.onClicked` in
`MenuManager::ExecuteCommand`, tab objects were constructed using a
hardcoded `kDontScrubTab` policy rather than evaluating extension
permissions. Consequently, tab details were not scrubbed for extensions
lacking corresponding permissions.
Update `MenuManager::ExecuteCommand` to determine tab scrubbing behavior
dynamically via `ExtensionTabUtil::GetScrubTabBehavior`. In addition,
invoke `ActiveTabPermissionGranter::GrantIfRequested` prior to tab
object construction so extensions with the `activeTab` permission
receive the appropriate tab data upon invocation.
Also add tests covering tab scrubbing behavior for context menus.
Bug: 513521516
Change-Id: I170a73d86dc77555ce1a1af85f14b99feaaa9764
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8382844
Reviewed-by: Devlin Cronin <rdevlin.cronin@chromium.org>
Commit-Queue: Tim <tjudkins@chromium.org>
Reviewed-by: Oliver Dunk <oliverdunk@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1700202}
---
diff --git a/chrome/browser/extensions/menu_manager.cc b/chrome/browser/extensions/menu_manager.cc
index 649e665..41b7719d 100644
--- a/chrome/browser/extensions/menu_manager.cc
+++ b/chrome/browser/extensions/menu_manager.cc
@@ -37,6 +37,7 @@
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_handlers/background_info.h"
+#include "extensions/common/mojom/context_type.mojom.h"
#include "extensions/common/mojom/event_dispatcher.mojom.h"
#include "ipc/constants.mojom.h"
#include "third_party/blink/public/mojom/context_menu/context_menu.mojom.h"
@@ -737,6 +738,14 @@
WebViewGuest::FromRenderFrameHost(render_frame_host);
#endif // BUILDFLAG(ENABLE_GUEST_VIEW)
+ // Note: web_contents are null in unit tests :(
+ if (extension && web_contents) {
+ if (auto* granter =
+ ActiveTabPermissionGranter::FromWebContents(web_contents)) {
+ granter->GrantIfRequested(extension);
+ }
+ }
+
base::ListValue args;
args.Append(std::move(properties));
@@ -747,15 +756,17 @@
// Note: web_contents are null in unit tests :(
if (web_contents) {
int frame_id = ExtensionApiFrameIdMap::GetFrameId(render_frame_host);
- if (frame_id != ExtensionApiFrameIdMap::kInvalidFrameId)
+ if (frame_id != ExtensionApiFrameIdMap::kInvalidFrameId) {
args[0].GetDict().Set("frameId", frame_id);
+ }
- // We intentionally don't scrub the tab data here, since the user chose to
- // invoke the extension on the page.
- // TODO(tjudkins) Potentially use GetScrubTabBehavior here to gate based
- // on permissions.
- ExtensionTabUtil::ScrubTabBehavior scrub_tab_behavior = {
- ExtensionTabUtil::kDontScrubTab, ExtensionTabUtil::kDontScrubTab};
+ // The contextMenus API is only available to privileged extension
+ // contexts.
+ constexpr mojom::ContextType context_type =
+ mojom::ContextType::kPrivilegedExtension;
+ ExtensionTabUtil::ScrubTabBehavior scrub_tab_behavior =
+ ExtensionTabUtil::GetScrubTabBehavior(extension, context_type,
+ web_contents);
args.Append(ExtensionTabUtil::CreateTabObject(
web_contents, scrub_tab_behavior, extension)
.ToValue());
@@ -776,15 +787,9 @@
item->SetChecked(checked);
args[0].GetDict().Set("checked", item->checked());
- if (extension)
+ if (extension) {
WriteToStorage(extension, item->id().extension_key);
- }
-
- // Note: web_contents are null in unit tests :(
- if (web_contents &&
- ActiveTabPermissionGranter::FromWebContents(web_contents)) {
- ActiveTabPermissionGranter::FromWebContents(web_contents)
- ->GrantIfRequested(extension);
+ }
}
{
// Dispatch to menu item's .onclick handler (this is the legacy API, from
diff --git a/chrome/browser/extensions/api/context_menus/context_menu_apitest.cc b/chrome/browser/extensions/api/context_menus/context_menu_apitest.cc
index 6d9c888a..fc2df13 100644
--- a/chrome/browser/extensions/api/context_menus/context_menu_apitest.cc
+++ b/chrome/browser/extensions/api/context_menus/context_menu_apitest.cc
@@ -4,14 +4,20 @@
#include <memory>
#include <optional>
+#include <string>
#include <utility>
+#include <vector>
+#include "base/json/json_reader.h"
#include "base/memory/raw_ptr.h"
+#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
+#include "base/values.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/extensions/context_menu_matcher.h"
#include "chrome/browser/extensions/extension_apitest.h"
+#include "chrome/browser/extensions/extension_context_menu_model.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/menu_manager.h"
#include "chrome/browser/profiles/profile.h"
@@ -29,8 +35,12 @@
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/extension_id.h"
+#include "extensions/common/mojom/api_permission_id.mojom-shared.h"
+#include "extensions/common/permissions/permissions_data.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
+#include "extensions/test/test_extension_dir.h"
+#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "ui/base/models/menu_model.h"
@@ -292,6 +302,314 @@
ASSERT_TRUE(RunExtensionTest("context_menus/item_ids")) << message_;
}
+namespace {
+
+// The background script used by the tab scrubbing tests below. Rather than
+// asserting on the click data itself, it echoes the `info` and `tab` objects
+// it was given back to the browser process, so that the expectations can live
+// in the tests themselves. `%s` is the context the menu item is shown in.
+constexpr char kScrubbingScriptTemplate[] = R"(
+ chrome.contextMenus.onClicked.addListener((info, tab) => {
+ chrome.test.sendMessage(JSON.stringify({info: info, tab: tab}));
+ });
+
+ chrome.contextMenus.create(
+ {id: 'test_item', title: 'Test Item', contexts: ['%s']},
+ () => chrome.test.sendMessage('created'));
+)";
+
+// Verifies that the sensitive metadata the extension is not allowed to see was
+// removed from `tab`.
+void ExpectTabScrubbed(const base::DictValue& tab) {
+ EXPECT_FALSE(tab.contains("url"));
+ EXPECT_FALSE(tab.contains("title"));
+ EXPECT_FALSE(tab.contains("favIconUrl"));
+ EXPECT_FALSE(tab.contains("pendingUrl"));
+ // Non-sensitive properties should still be present. This also guards against
+ // an unexpected object trivially satisfying the checks above.
+ EXPECT_TRUE(tab.FindInt("id").has_value());
+}
+
+// Verifies that `tab` still has the metadata for the page at `url`.
+void ExpectTabNotScrubbed(const base::DictValue& tab, const GURL& url) {
+ const std::string* tab_url = tab.FindString("url");
+ ASSERT_TRUE(tab_url);
+ EXPECT_EQ(url.spec(), *tab_url);
+ const std::string* title = tab.FindString("title");
+ ASSERT_TRUE(title);
+ // simple.html has a title of "OK".
+ EXPECT_EQ("OK", *title);
+ EXPECT_TRUE(tab.FindInt("id").has_value());
+}
+
+// Verifies the `pageUrl` the extension was given for the click.
+void ExpectPageUrl(const base::DictValue& info, const GURL& url) {
+ const std::string* page_url = info.FindString("pageUrl");
+ ASSERT_TRUE(page_url);
+ EXPECT_EQ(url.spec(), *page_url);
+}
+
+} // namespace
+
+class ContextMenusTabScrubbingTest : public ExtensionContextMenuApiTest {
+ public:
+ // The context the extension's menu item is shown in.
+ enum class MenuContext {
+ kPage,
+ kAction,
+ };
+
+ // The `info` and `tab` objects an extension was given for a menu click.
+ struct ClickData {
+ base::DictValue info;
+ base::DictValue tab;
+ };
+
+ void SetUpOnMainThread() override {
+ ExtensionContextMenuApiTest::SetUpOnMainThread();
+ host_resolver()->AddRule("*", "127.0.0.1");
+ ASSERT_TRUE(embedded_test_server()->Start());
+ }
+
+ // Loads an extension with a single context menu item shown in
+ // `menu_context`, granted the contextMenus permission along with any
+ // `extra_permissions` and `host_permissions`. Returns nullptr on failure.
+ const Extension* LoadScrubbingExtension(
+ MenuContext menu_context,
+ const std::vector<std::string>& extra_permissions = {},
+ const std::vector<std::string>& host_permissions = {}) {
+ base::ListValue permissions;
+ permissions.Append("contextMenus");
+ for (const std::string& permission : extra_permissions) {
+ permissions.Append(permission);
+ }
+
+ base::DictValue manifest =
+ base::DictValue()
+ .Set("name", "ContextMenus Tab Scrubbing Test")
+ .Set("version", "1")
+ .Set("manifest_version", 3)
+ .Set("permissions", std::move(permissions))
+ .Set("background",
+ base::DictValue().Set("service_worker", "sw.js"));
+
+ if (!host_permissions.empty()) {
+ base::ListValue hosts;
+ for (const std::string& host : host_permissions) {
+ hosts.Append(host);
+ }
+ manifest.Set("host_permissions", std::move(hosts));
+ }
+
+ const char* context_name = nullptr;
+ switch (menu_context) {
+ case MenuContext::kPage:
+ context_name = "page";
+ break;
+ case MenuContext::kAction:
+ // Items shown in the action context require the extension to have an
+ // action to show them on.
+ manifest.Set("action", base::DictValue());
+ context_name = "action";
+ break;
+ }
+
+ test_dir_.WriteManifest(manifest);
+ test_dir_.WriteFile(
+ FILE_PATH_LITERAL("sw.js"),
+ base::StringPrintf(kScrubbingScriptTemplate, context_name));
+
+ // Wait for the item to be created, otherwise it won't be in the menu when
+ // one of the Click*ContextMenuItem() methods shows it.
+ ExtensionTestMessageListener created_listener("created");
+ const Extension* extension = LoadExtension(test_dir_.UnpackedPath());
+ if (!extension || !created_listener.WaitUntilSatisfied()) {
+ return nullptr;
+ }
+ return extension;
+ }
+
+ GURL GetTestUrl(std::string_view host) {
+ return embedded_test_server()->GetURL(host, "/simple.html");
+ }
+
+ // Shows a page context menu on the active tab and activates the extension's
+ // item in it, returning the data the extension received for the click.
+ std::optional<ClickData> ClickPageContextMenuItem() {
+ content::RenderFrameHost* frame =
+ GetActiveWebContents()->GetPrimaryMainFrame();
+ content::ContextMenuParams params;
+ params.page_url = frame->GetLastCommittedURL();
+
+ const int command_id =
+ ContextMenuMatcher::ConvertToExtensionsCustomCommandId(/*id=*/0);
+
+ ExtensionTestMessageListener click_listener;
+#if BUILDFLAG(IS_ANDROID)
+ ExtensionMenuModel menu(*frame, params);
+ menu.PopulateModel();
+#else
+ TestRenderViewContextMenu menu(*frame, params);
+ menu.Init();
+#endif
+ EXPECT_TRUE(menu.IsCommandIdVisible(command_id));
+ EXPECT_TRUE(menu.IsCommandIdEnabled(command_id));
+ menu.ExecuteCommand(command_id, /*event_flags=*/0);
+
+ return WaitForClickData(click_listener);
+ }
+
+ // Shows the context menu for the extension's toolbar action and activates
+ // the extension's item in it, returning the data the extension received for
+ // the click.
+ std::optional<ClickData> ClickActionContextMenuItem(
+ const Extension& extension) {
+ const int command_id =
+ ContextMenuMatcher::ConvertToExtensionsCustomCommandId(/*id=*/0);
+ ExtensionContextMenuModel menu(
+ &extension, browser_window_interface(),
+ /*is_pinned=*/true, /*delegate=*/nullptr,
+ /*can_show_icon_in_toolbar=*/true,
+ ExtensionContextMenuModel::ContextMenuSource::kToolbarAction);
+ EXPECT_TRUE(menu.GetIndexOfCommandId(command_id).has_value());
+
+ ExtensionTestMessageListener click_listener;
+ menu.ExecuteCommand(command_id, /*event_flags=*/0);
+
+ return WaitForClickData(click_listener);
+ }
+
+ private:
+ // Waits for the extension to echo back the data for a menu item click.
+ std::optional<ClickData> WaitForClickData(
+ ExtensionTestMessageListener& listener) {
+ if (!listener.WaitUntilSatisfied()) {
+ ADD_FAILURE() << "Never received a click from the extension.";
+ return std::nullopt;
+ }
+
+ std::optional<base::DictValue> click_data =
+ base::JSONReader::ReadDict(listener.message(), base::JSON_PARSE_RFC);
+ base::DictValue* info = click_data ? click_data->FindDict("info") : nullptr;
+ base::DictValue* tab = click_data ? click_data->FindDict("tab") : nullptr;
+ if (!info || !tab) {
+ ADD_FAILURE() << "Unexpected click data: " << listener.message();
+ return std::nullopt;
+ }
+ return ClickData{std::move(*info), std::move(*tab)};
+ }
+
+ // The files for the extension loaded by LoadScrubbingExtension(). Owned by
+ // the fixture so that they outlive the loaded extension.
+ TestExtensionDir test_dir_;
+};
+
+// An extension without tab or host permissions should have sensitive tab
+// metadata scrubbed when its menu item is invoked from the toolbar action.
+IN_PROC_BROWSER_TEST_F(ContextMenusTabScrubbingTest,
+ ActionContextMenuWithoutPermissionsScrubsTab) {
+ const Extension* extension = LoadScrubbingExtension(MenuContext::kAction);
+ ASSERT_TRUE(extension);
+ ASSERT_TRUE(
+ NavigateToURL(GetActiveWebContents(), GetTestUrl("example.test")));
+
+ std::optional<ClickData> click_data = ClickActionContextMenuItem(*extension);
+ ASSERT_TRUE(click_data);
+ // `pageUrl` is only provided for items invoked on a page.
+ EXPECT_FALSE(click_data->info.contains("pageUrl"));
+ ExpectTabScrubbed(click_data->tab);
+}
+
+// An extension with the activeTab permission is granted access to the tab when
+// its menu item is invoked, so the tab should not be scrubbed.
+IN_PROC_BROWSER_TEST_F(ContextMenusTabScrubbingTest,
+ ActionContextMenuWithActiveTabDoesNotScrubTab) {
+ const Extension* extension =
+ LoadScrubbingExtension(MenuContext::kAction, {"activeTab"});
+ ASSERT_TRUE(extension);
+ const GURL url = GetTestUrl("example.test");
+ ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), url));
+
+ std::optional<ClickData> click_data = ClickActionContextMenuItem(*extension);
+ ASSERT_TRUE(click_data);
+ EXPECT_FALSE(click_data->info.contains("pageUrl"));
+ ExpectTabNotScrubbed(click_data->tab, url);
+}
+
+// An extension without tab or host permissions should have sensitive tab
+// metadata scrubbed when its menu item is invoked from a page context menu,
+// though it is still told which page the click happened on.
+IN_PROC_BROWSER_TEST_F(ContextMenusTabScrubbingTest,
+ PageContextMenuWithoutPermissionsScrubsTab) {
+ ASSERT_TRUE(LoadScrubbingExtension(MenuContext::kPage));
+ const GURL url = GetTestUrl("example.test");
+ ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), url));
+
+ std::optional<ClickData> click_data = ClickPageContextMenuItem();
+ ASSERT_TRUE(click_data);
+ ExpectPageUrl(click_data->info, url);
+ ExpectTabScrubbed(click_data->tab);
+}
+
+// An extension with the activeTab permission is granted access to the tab when
+// its menu item is invoked, so the tab should not be scrubbed.
+IN_PROC_BROWSER_TEST_F(ContextMenusTabScrubbingTest,
+ PageContextMenuWithActiveTabDoesNotScrubTab) {
+ ASSERT_TRUE(LoadScrubbingExtension(MenuContext::kPage, {"activeTab"}));
+ const GURL url = GetTestUrl("example.test");
+ ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), url));
+
+ std::optional<ClickData> click_data = ClickPageContextMenuItem();
+ ASSERT_TRUE(click_data);
+ ExpectPageUrl(click_data->info, url);
+ ExpectTabNotScrubbed(click_data->tab, url);
... (file diff truncated)
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/extensions/api/context_menus/context_menu_apitest.cc b/chrome/browser/extensions/api/context_menus/context_menu_apitest.cc
index 6d9c888a..fc2df13 100644
--- a/chrome/browser/extensions/api/context_menus/context_menu_apitest.cc
+++ b/chrome/browser/extensions/api/context_menus/context_menu_apitest.cc
@@ -4,14 +4,20 @@
#include <memory>
#include <optional>
+#include <string>
#include <utility>
+#include <vector>
+#include "base/json/json_reader.h"
#include "base/memory/raw_ptr.h"
+#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
+#include "base/values.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/extensions/context_menu_matcher.h"
#include "chrome/browser/extensions/extension_apitest.h"
+#include "chrome/browser/extensions/extension_context_menu_model.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/menu_manager.h"
#include "chrome/browser/profiles/profile.h"
@@ -29,8 +35,12 @@
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/extension_id.h"
+#include "extensions/common/mojom/api_permission_id.mojom-shared.h"
+#include "extensions/common/permissions/permissions_data.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
+#include "extensions/test/test_extension_dir.h"
+#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "ui/base/models/menu_model.h"
@@ -292,6 +302,314 @@
ASSERT_TRUE(RunExtensionTest("context_menus/item_ids")) << message_;
}
+namespace {
+
+// The background script used by the tab scrubbing tests below. Rather than
+// asserting on the click data itself, it echoes the `info` and `tab` objects
+// it was given back to the browser process, so that the expectations can live
+// in the tests themselves. `%s` is the context the menu item is shown in.
+constexpr char kScrubbingScriptTemplate[] = R"(
+ chrome.contextMenus.onClicked.addListener((info, tab) => {
+ chrome.test.sendMessage(JSON.stringify({info: info, tab: tab}));
+ });
+
+ chrome.contextMenus.create(
+ {id: 'test_item', title: 'Test Item', contexts: ['%s']},
+ () => chrome.test.sendMessage('created'));
+)";
+
+// Verifies that the sensitive metadata the extension is not allowed to see was
+// removed from `tab`.
+void ExpectTabScrubbed(const base::DictValue& tab) {
+ EXPECT_FALSE(tab.contains("url"));
+ EXPECT_FALSE(tab.contains("title"));
+ EXPECT_FALSE(tab.contains("favIconUrl"));
+ EXPECT_FALSE(tab.contains("pendingUrl"));
+ // Non-sensitive properties should still be present. This also guards against
+ // an unexpected object trivially satisfying the checks above.
+ EXPECT_TRUE(tab.FindInt("id").has_value());
+}
+
+// Verifies that `tab` still has the metadata for the page at `url`.
+void ExpectTabNotScrubbed(const base::DictValue& tab, const GURL& url) {
+ const std::string* tab_url = tab.FindString("url");
+ ASSERT_TRUE(tab_url);
+ EXPECT_EQ(url.spec(), *tab_url);
+ const std::string* title = tab.FindString("title");
+ ASSERT_TRUE(title);
+ // simple.html has a title of "OK".
+ EXPECT_EQ("OK", *title);
+ EXPECT_TRUE(tab.FindInt("id").has_value());
+}
+
+// Verifies the `pageUrl` the extension was given for the click.
+void ExpectPageUrl(const base::DictValue& info, const GURL& url) {
+ const std::string* page_url = info.FindString("pageUrl");
+ ASSERT_TRUE(page_url);
+ EXPECT_EQ(url.spec(), *page_url);
+}
+
+} // namespace
+
+class ContextMenusTabScrubbingTest : public ExtensionContextMenuApiTest {
+ public:
+ // The context the extension's menu item is shown in.
+ enum class MenuContext {
+ kPage,
+ kAction,
+ };
+
+ // The `info` and `tab` objects an extension was given for a menu click.
+ struct ClickData {
+ base::DictValue info;
+ base::DictValue tab;
+ };
+
+ void SetUpOnMainThread() override {
+ ExtensionContextMenuApiTest::SetUpOnMainThread();
+ host_resolver()->AddRule("*", "127.0.0.1");
+ ASSERT_TRUE(embedded_test_server()->Start());
+ }
+
+ // Loads an extension with a single context menu item shown in
+ // `menu_context`, granted the contextMenus permission along with any
+ // `extra_permissions` and `host_permissions`. Returns nullptr on failure.
+ const Extension* LoadScrubbingExtension(
+ MenuContext menu_context,
+ const std::vector<std::string>& extra_permissions = {},
+ const std::vector<std::string>& host_permissions = {}) {
+ base::ListValue permissions;
+ permissions.Append("contextMenus");
+ for (const std::string& permission : extra_permissions) {
+ permissions.Append(permission);
+ }
+
+ base::DictValue manifest =
+ base::DictValue()
+ .Set("name", "ContextMenus Tab Scrubbing Test")
+ .Set("version", "1")
+ .Set("manifest_version", 3)
+ .Set("permissions", std::move(permissions))
+ .Set("background",
+ base::DictValue().Set("service_worker", "sw.js"));
+
+ if (!host_permissions.empty()) {
+ base::ListValue hosts;
+ for (const std::string& host : host_permissions) {
+ hosts.Append(host);
+ }
+ manifest.Set("host_permissions", std::move(hosts));
+ }
+
+ const char* context_name = nullptr;
+ switch (menu_context) {
+ case MenuContext::kPage:
+ context_name = "page";
+ break;
+ case MenuContext::kAction:
+ // Items shown in the action context require the extension to have an
+ // action to show them on.
+ manifest.Set("action", base::DictValue());
+ context_name = "action";
+ break;
+ }
+
+ test_dir_.WriteManifest(manifest);
+ test_dir_.WriteFile(
+ FILE_PATH_LITERAL("sw.js"),
+ base::StringPrintf(kScrubbingScriptTemplate, context_name));
+
+ // Wait for the item to be created, otherwise it won't be in the menu when
+ // one of the Click*ContextMenuItem() methods shows it.
+ ExtensionTestMessageListener created_listener("created");
+ const Extension* extension = LoadExtension(test_dir_.UnpackedPath());
+ if (!extension || !created_listener.WaitUntilSatisfied()) {
+ return nullptr;
+ }
+ return extension;
+ }
+
+ GURL GetTestUrl(std::string_view host) {
+ return embedded_test_server()->GetURL(host, "/simple.html");
+ }
+
+ // Shows a page context menu on the active tab and activates the extension's
+ // item in it, returning the data the extension received for the click.
+ std::optional<ClickData> ClickPageContextMenuItem() {
+ content::RenderFrameHost* frame =
+ GetActiveWebContents()->GetPrimaryMainFrame();
+ content::ContextMenuParams params;
+ params.page_url = frame->GetLastCommittedURL();
+
+ const int command_id =
+ ContextMenuMatcher::ConvertToExtensionsCustomCommandId(/*id=*/0);
+
+ ExtensionTestMessageListener click_listener;
+#if BUILDFLAG(IS_ANDROID)
+ ExtensionMenuModel menu(*frame, params);
+ menu.PopulateModel();
+#else
+ TestRenderViewContextMenu menu(*frame, params);
+ menu.Init();
+#endif
+ EXPECT_TRUE(menu.IsCommandIdVisible(command_id));
+ EXPECT_TRUE(menu.IsCommandIdEnabled(command_id));
+ menu.ExecuteCommand(command_id, /*event_flags=*/0);
+
+ return WaitForClickData(click_listener);
+ }
+
+ // Shows the context menu for the extension's toolbar action and activates
+ // the extension's item in it, returning the data the extension received for
+ // the click.
+ std::optional<ClickData> ClickActionContextMenuItem(
+ const Extension& extension) {
+ const int command_id =
+ ContextMenuMatcher::ConvertToExtensionsCustomCommandId(/*id=*/0);
+ ExtensionContextMenuModel menu(
+ &extension, browser_window_interface(),
+ /*is_pinned=*/true, /*delegate=*/nullptr,
+ /*can_show_icon_in_toolbar=*/true,
+ ExtensionContextMenuModel::ContextMenuSource::kToolbarAction);
+ EXPECT_TRUE(menu.GetIndexOfCommandId(command_id).has_value());
+
+ ExtensionTestMessageListener click_listener;
+ menu.ExecuteCommand(command_id, /*event_flags=*/0);
+
+ return WaitForClickData(click_listener);
+ }
+
+ private:
+ // Waits for the extension to echo back the data for a menu item click.
+ std::optional<ClickData> WaitForClickData(
+ ExtensionTestMessageListener& listener) {
+ if (!listener.WaitUntilSatisfied()) {
+ ADD_FAILURE() << "Never received a click from the extension.";
+ return std::nullopt;
+ }
+
+ std::optional<base::DictValue> click_data =
+ base::JSONReader::ReadDict(listener.message(), base::JSON_PARSE_RFC);
+ base::DictValue* info = click_data ? click_data->FindDict("info") : nullptr;
+ base::DictValue* tab = click_data ? click_data->FindDict("tab") : nullptr;
+ if (!info || !tab) {
+ ADD_FAILURE() << "Unexpected click data: " << listener.message();
+ return std::nullopt;
+ }
+ return ClickData{std::move(*info), std::move(*tab)};
+ }
+
+ // The files for the extension loaded by LoadScrubbingExtension(). Owned by
+ // the fixture so that they outlive the loaded extension.
+ TestExtensionDir test_dir_;
+};
+
+// An extension without tab or host permissions should have sensitive tab
+// metadata scrubbed when its menu item is invoked from the toolbar action.
+IN_PROC_BROWSER_TEST_F(ContextMenusTabScrubbingTest,
+ ActionContextMenuWithoutPermissionsScrubsTab) {
+ const Extension* extension = LoadScrubbingExtension(MenuContext::kAction);
+ ASSERT_TRUE(extension);
+ ASSERT_TRUE(
+ NavigateToURL(GetActiveWebContents(), GetTestUrl("example.test")));
+
+ std::optional<ClickData> click_data = ClickActionContextMenuItem(*extension);
+ ASSERT_TRUE(click_data);
+ // `pageUrl` is only provided for items invoked on a page.
+ EXPECT_FALSE(click_data->info.contains("pageUrl"));
+ ExpectTabScrubbed(click_data->tab);
+}
+
+// An extension with the activeTab permission is granted access to the tab when
+// its menu item is invoked, so the tab should not be scrubbed.
+IN_PROC_BROWSER_TEST_F(ContextMenusTabScrubbingTest,
+ ActionContextMenuWithActiveTabDoesNotScrubTab) {
+ const Extension* extension =
+ LoadScrubbingExtension(MenuContext::kAction, {"activeTab"});
+ ASSERT_TRUE(extension);
+ const GURL url = GetTestUrl("example.test");
+ ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), url));
+
+ std::optional<ClickData> click_data = ClickActionContextMenuItem(*extension);
+ ASSERT_TRUE(click_data);
+ EXPECT_FALSE(click_data->info.contains("pageUrl"));
+ ExpectTabNotScrubbed(click_data->tab, url);
+}
+
+// An extension without tab or host permissions should have sensitive tab
+// metadata scrubbed when its menu item is invoked from a page context menu,
+// though it is still told which page the click happened on.
+IN_PROC_BROWSER_TEST_F(ContextMenusTabScrubbingTest,
+ PageContextMenuWithoutPermissionsScrubsTab) {
+ ASSERT_TRUE(LoadScrubbingExtension(MenuContext::kPage));
+ const GURL url = GetTestUrl("example.test");
+ ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), url));
+
+ std::optional<ClickData> click_data = ClickPageContextMenuItem();
+ ASSERT_TRUE(click_data);
+ ExpectPageUrl(click_data->info, url);
+ ExpectTabScrubbed(click_data->tab);
+}
+
+// An extension with the activeTab permission is granted access to the tab when
+// its menu item is invoked, so the tab should not be scrubbed.
+IN_PROC_BROWSER_TEST_F(ContextMenusTabScrubbingTest,
+ PageContextMenuWithActiveTabDoesNotScrubTab) {
+ ASSERT_TRUE(LoadScrubbingExtension(MenuContext::kPage, {"activeTab"}));
+ const GURL url = GetTestUrl("example.test");
+ ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), url));
+
+ std::optional<ClickData> click_data = ClickPageContextMenuItem();
... (truncated)
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page