CVE-2026-13957
Overview
Files Changed
chrome/browser/extensions/extension_context_menu_model.ccchrome/browser/extensions/extension_context_menu_model_browsertest.ccchrome/browser/extensions/permissions/host_access_requests_helper_unittest.ccchrome/browser/extensions/permissions/site_permissions_helper_browsertest.cc
Patch
From e6f8ab61283e2eca8ee75188cfb0421215215292 Mon Sep 17 00:00:00 2001 From: Eva Su <[email protected]> Date: Thu, 28 May 2026 16:58:51 -0700 Subject: [PATCH] [Extensions] Fix TOCTOU vulnerability in site access toggle This CL fixes a Time-of-Check Time-of-Use (TOCTOU) vulnerability in the extensions menu site access toggle, and implements the suggested fix by binding the site-access action to the origin that was active when the user initiated the click. Previously, the target origin for permission changes was resolved from the active WebContents at the moment of the click. Because the menu remains open during page navigations, an attacker could initiate a navigation to a sensitive site right before the click was processed, leading to the extension being granted access to the new, sensitive origin instead of the original one. We also add checks for IsRestrictedUrl to prevent potential race conditions where the UI to allow changing permission settings for a site even if the extension is fundamentally barred from running there. These checks ensure that permission changes are only considered for valid, non-restricted origins and that the UI state correctly reflects that permissions cannot be granted or altered on these sensitive sites. For full consistency, we should also update AllowHostAccessRequest() to use the target_origin, however, this is lower priority since it’s typically triggered by a more immediate UI action, but I’ll handle this separately in a follow-up CL (crrev.com/c/7865583) since this one is already getting quite large. Fixed: 513553557 Change-Id: I7bb8dbe11665cef4a1076fd748d256683a9035f8 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7853880 Commit-Queue: Eva Su <[email protected]> Reviewed-by: Tim <[email protected]> Cr-Commit-Position: refs/heads/main@{#1638068} --- diff --git a/chrome/browser/extensions/extension_context_menu_model.cc b/chrome/browser/extensions/extension_context_menu_model.cc index dec7b8f..1d161ede 100644 --- a/chrome/browser/extensions/extension_context_menu_model.cc +++ b/chrome/browser/extensions/extension_context_menu_model.cc @@ -401,6 +401,10 @@ command_id == PAGE_ACCESS_RUN_ON_SITE || command_id == PAGE_ACCESS_RUN_ON_ALL_SITES) { auto* permissions = PermissionsManager::Get(profile_); + if (extension->permissions_data()->IsRestrictedUrl(origin_.GetURL(), + nullptr)) { + return false; + } PermissionsManager::UserSiteAccess current_access = permissions->GetUserSiteAccess(*extension, origin_.GetURL()); return current_access == CommandIdToSiteAccess(command_id); @@ -634,7 +638,7 @@ SitePermissionsHelper permissions(profile_); permissions.UpdateSiteAccess(*extension, web_contents, - CommandIdToSiteAccess(command_id)); + CommandIdToSiteAccess(command_id), origin_); break; } case PAGE_ACCESS_PERMISSIONS_PAGE: diff --git a/chrome/browser/extensions/extension_context_menu_model_browsertest.cc b/chrome/browser/extensions/extension_context_menu_model_browsertest.cc index 3143df4..236db25 100644 --- a/chrome/browser/extensions/extension_context_menu_model_browsertest.cc +++ b/chrome/browser/extensions/extension_context_menu_model_browsertest.cc @@ -1560,8 +1560,9 @@ // Update kOriginalUrl to have "on site" site access. This will make all other // non-restricted urls to have "on click" site access. SitePermissionsHelper permissions(profile()); - permissions.UpdateSiteAccess(*extension, web_contents, - PermissionsManager::UserSiteAccess::kOnSite); + permissions.UpdateSiteAccess( + *extension, web_contents, PermissionsManager::UserSiteAccess::kOnSite, + web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin()); PermissionsManager* permissions_manager = PermissionsManager::Get(profile()); EXPECT_EQ(permissions_manager->GetUserSiteAccess(*extension, kOriginalUrl), diff --git a/chrome/browser/extensions/permissions/host_access_requests_helper_unittest.cc b/chrome/browser/extensions/permissions/host_access_requests_helper_unittest.cc index 12bbb76..1625655 100644 --- a/chrome/browser/extensions/permissions/host_access_requests_helper_unittest.cc +++ b/chrome/browser/extensions/permissions/host_access_requests_helper_unittest.cc @@ -360,8 +360,9 @@ // Grant "always on this host" access to the extension. PermissionsManagerWaiter waiter(PermissionsManager::Get(profile())); SitePermissionsHelper permissions(profile()); - permissions.UpdateSiteAccess(*extension, web_contents, - PermissionsManager::UserSiteAccess::kOnSite); + permissions.UpdateSiteAccess( + *extension, web_contents, PermissionsManager::UserSiteAccess::kOnSite, + web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin()); waiter.WaitForExtensionPermissionsUpdate(); // Request should be removed since extension has granted host access. diff --git a/chrome/browser/extensions/permissions/site_permissions_helper_browsertest.cc b/chrome/browser/extensions/permissions/site_permissions_helper_browsertest.cc index 48bf511..d7ec7c5 100644 --- a/chrome/browser/extensions/permissions/site_permissions_helper_browsertest.cc +++ b/chrome/browser/extensions/permissions/site_permissions_helper_browsertest.cc @@ -26,6 +26,7 @@ #include "extensions/test/extension_test_message_listener.h" #include "net/dns/mock_host_resolver.h" #include "testing/gtest/include/gtest/gtest.h" +#include "url/origin.h" static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE)); @@ -162,8 +163,9 @@ ReloadPageDialogController::AcceptDialogForTesting(true); // on all sites -> on site - permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(), - UserSiteAccess::kOnSite); + permissions_helper_->UpdateSiteAccess( + *extension_, GetActiveWebContents(), UserSiteAccess::kOnSite, + GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin()); EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_), UserSiteAccess::kOnSite); // We assume that there is only ever one action that wants to run for the test @@ -173,8 +175,9 @@ ASSERT_FALSE(ExtensionWantsToRun()); // on site -> on-click (refresh needed due to revoking permissions) - permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(), - UserSiteAccess::kOnClick); + permissions_helper_->UpdateSiteAccess( + *extension_, GetActiveWebContents(), UserSiteAccess::kOnClick, + GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin()); EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_), UserSiteAccess::kOnClick); ASSERT_TRUE(WaitForReloadToFinish()); @@ -183,8 +186,9 @@ // on click -> on site (refresh needed due to script wanting to load at // start) - permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(), - UserSiteAccess::kOnSite); + permissions_helper_->UpdateSiteAccess( + *extension_, GetActiveWebContents(), UserSiteAccess::kOnSite, + GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin()); EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_), UserSiteAccess::kOnSite); ASSERT_TRUE(WaitForReloadToFinish()); @@ -192,16 +196,18 @@ ASSERT_FALSE(ExtensionWantsToRun()); // on site -> on all sites - permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(), - UserSiteAccess::kOnAllSites); + permissions_helper_->UpdateSiteAccess( + *extension_, GetActiveWebContents(), UserSiteAccess::kOnAllSites, + GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin()); EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_), UserSiteAccess::kOnAllSites); ASSERT_TRUE(ContentScriptInjected()); ASSERT_FALSE(ExtensionWantsToRun()); // on all sites -> on-click (refresh needed due to revoking permissions) - permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(), - UserSiteAccess::kOnClick); + permissions_helper_->UpdateSiteAccess( + *extension_, GetActiveWebContents(), UserSiteAccess::kOnClick, + GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin()); EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_), UserSiteAccess::kOnClick); EXPECT_TRUE(WaitForReloadToFinish()); @@ -236,8 +242,9 @@ ReloadPageDialogController::AcceptDialogForTesting(true); // on all sites -> on site - permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(), - UserSiteAccess::kOnSite); + permissions_helper_->UpdateSiteAccess( + *extension_, GetActiveWebContents(), UserSiteAccess::kOnSite, + GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin()); EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_), UserSiteAccess::kOnSite); // We assume that there is only ever one action that wants to run for the test @@ -247,8 +254,9 @@ ASSERT_FALSE(ExtensionWantsToRun()); // on site -> on-click (refresh needed due to revoking permissions) - permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(), - UserSiteAccess::kOnClick); + permissions_helper_->UpdateSiteAccess( + *extension_, GetActiveWebContents(), UserSiteAccess::kOnClick, + GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin()); EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_), UserSiteAccess::kOnClick); EXPECT_TRUE(ContentScriptInjected() && !ExtensionWantsToRun()); @@ -258,8 +266,9 @@ // on click -> on site (refresh needed due to script wanting to load at // start) - permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(), - UserSiteAccess::kOnSite); + permissions_helper_->UpdateSiteAccess( + *extension_, GetActiveWebContents(), UserSiteAccess::kOnSite, + GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin()); EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_), UserSiteAccess::kOnSite);
Regression Test / PoC
diff --git a/chrome/browser/extensions/extension_context_menu_model_browsertest.cc b/chrome/browser/extensions/extension_context_menu_model_browsertest.cc
index 3143df4..236db25 100644
--- a/chrome/browser/extensions/extension_context_menu_model_browsertest.cc
+++ b/chrome/browser/extensions/extension_context_menu_model_browsertest.cc
@@ -1560,8 +1560,9 @@
// Update kOriginalUrl to have "on site" site access. This will make all other
// non-restricted urls to have "on click" site access.
SitePermissionsHelper permissions(profile());
- permissions.UpdateSiteAccess(*extension, web_contents,
- PermissionsManager::UserSiteAccess::kOnSite);
+ permissions.UpdateSiteAccess(
+ *extension, web_contents, PermissionsManager::UserSiteAccess::kOnSite,
+ web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin());
PermissionsManager* permissions_manager = PermissionsManager::Get(profile());
EXPECT_EQ(permissions_manager->GetUserSiteAccess(*extension, kOriginalUrl),
diff --git a/chrome/browser/extensions/permissions/host_access_requests_helper_unittest.cc b/chrome/browser/extensions/permissions/host_access_requests_helper_unittest.cc
index 12bbb76..1625655 100644
--- a/chrome/browser/extensions/permissions/host_access_requests_helper_unittest.cc
+++ b/chrome/browser/extensions/permissions/host_access_requests_helper_unittest.cc
@@ -360,8 +360,9 @@
// Grant "always on this host" access to the extension.
PermissionsManagerWaiter waiter(PermissionsManager::Get(profile()));
SitePermissionsHelper permissions(profile());
- permissions.UpdateSiteAccess(*extension, web_contents,
- PermissionsManager::UserSiteAccess::kOnSite);
+ permissions.UpdateSiteAccess(
+ *extension, web_contents, PermissionsManager::UserSiteAccess::kOnSite,
+ web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin());
waiter.WaitForExtensionPermissionsUpdate();
// Request should be removed since extension has granted host access.
diff --git a/chrome/browser/extensions/permissions/site_permissions_helper_browsertest.cc b/chrome/browser/extensions/permissions/site_permissions_helper_browsertest.cc
index 48bf511..d7ec7c5 100644
--- a/chrome/browser/extensions/permissions/site_permissions_helper_browsertest.cc
+++ b/chrome/browser/extensions/permissions/site_permissions_helper_browsertest.cc
@@ -26,6 +26,7 @@
#include "extensions/test/extension_test_message_listener.h"
#include "net/dns/mock_host_resolver.h"
#include "testing/gtest/include/gtest/gtest.h"
+#include "url/origin.h"
static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));
@@ -162,8 +163,9 @@
ReloadPageDialogController::AcceptDialogForTesting(true);
// on all sites -> on site
- permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnSite);
+ permissions_helper_->UpdateSiteAccess(
+ *extension_, GetActiveWebContents(), UserSiteAccess::kOnSite,
+ GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnSite);
// We assume that there is only ever one action that wants to run for the test
@@ -173,8 +175,9 @@
ASSERT_FALSE(ExtensionWantsToRun());
// on site -> on-click (refresh needed due to revoking permissions)
- permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnClick);
+ permissions_helper_->UpdateSiteAccess(
+ *extension_, GetActiveWebContents(), UserSiteAccess::kOnClick,
+ GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnClick);
ASSERT_TRUE(WaitForReloadToFinish());
@@ -183,8 +186,9 @@
// on click -> on site (refresh needed due to script wanting to load at
// start)
- permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnSite);
+ permissions_helper_->UpdateSiteAccess(
+ *extension_, GetActiveWebContents(), UserSiteAccess::kOnSite,
+ GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnSite);
ASSERT_TRUE(WaitForReloadToFinish());
@@ -192,16 +196,18 @@
ASSERT_FALSE(ExtensionWantsToRun());
// on site -> on all sites
- permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnAllSites);
+ permissions_helper_->UpdateSiteAccess(
+ *extension_, GetActiveWebContents(), UserSiteAccess::kOnAllSites,
+ GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnAllSites);
ASSERT_TRUE(ContentScriptInjected());
ASSERT_FALSE(ExtensionWantsToRun());
// on all sites -> on-click (refresh needed due to revoking permissions)
- permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnClick);
+ permissions_helper_->UpdateSiteAccess(
+ *extension_, GetActiveWebContents(), UserSiteAccess::kOnClick,
+ GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnClick);
EXPECT_TRUE(WaitForReloadToFinish());
@@ -236,8 +242,9 @@
ReloadPageDialogController::AcceptDialogForTesting(true);
// on all sites -> on site
- permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnSite);
+ permissions_helper_->UpdateSiteAccess(
+ *extension_, GetActiveWebContents(), UserSiteAccess::kOnSite,
+ GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnSite);
// We assume that there is only ever one action that wants to run for the test
@@ -247,8 +254,9 @@
ASSERT_FALSE(ExtensionWantsToRun());
// on site -> on-click (refresh needed due to revoking permissions)
- permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnClick);
+ permissions_helper_->UpdateSiteAccess(
+ *extension_, GetActiveWebContents(), UserSiteAccess::kOnClick,
+ GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnClick);
EXPECT_TRUE(ContentScriptInjected() && !ExtensionWantsToRun());
@@ -258,8 +266,9 @@
// on click -> on site (refresh needed due to script wanting to load at
// start)
- permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnSite);
+ permissions_helper_->UpdateSiteAccess(
+ *extension_, GetActiveWebContents(), UserSiteAccess::kOnSite,
+ GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnSite);
EXPECT_TRUE(!ContentScriptInjected() && ExtensionWantsToRun());
@@ -268,16 +277,18 @@
ASSERT_FALSE(ExtensionWantsToRun());
// on site -> on all sites
- permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnAllSites);
+ permissions_helper_->UpdateSiteAccess(
+ *extension_, GetActiveWebContents(), UserSiteAccess::kOnAllSites,
+ GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnAllSites);
ASSERT_TRUE(ContentScriptInjected());
ASSERT_FALSE(ExtensionWantsToRun());
// on all sites -> on-click (refresh needed due to revoking permissions)
- permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnClick);
+ permissions_helper_->UpdateSiteAccess(
+ *extension_, GetActiveWebContents(), UserSiteAccess::kOnClick,
+ GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnClick);
EXPECT_TRUE(ContentScriptInjected() && !ExtensionWantsToRun());
@@ -351,7 +362,10 @@
// on all sites -> on click (revokes access)
BlockedActionWaiter blocked_action_waiter(active_action_runner());
permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnClick);
+ UserSiteAccess::kOnClick,
+ GetActiveWebContents()
+ ->GetPrimaryMainFrame()
+ ->GetLastCommittedOrigin());
ASSERT_EQ(
permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnClick);
@@ -366,8 +380,9 @@
ExtensionTestMessageListener listener("injection succeeded");
// on click -> on site (grants site access and active tab permission)
- permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnSite);
+ permissions_helper_->UpdateSiteAccess(
+ *extension_, GetActiveWebContents(), UserSiteAccess::kOnSite,
+ GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
ASSERT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnSite);
ASSERT_EQ(permissions_helper_->GetSiteInteraction(*extension_,
@@ -382,7 +397,10 @@
// permissions)
BlockedActionWaiter blocked_action_waiter(active_action_runner());
permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnClick);
+ UserSiteAccess::kOnClick,
+ GetActiveWebContents()
+ ->GetPrimaryMainFrame()
+ ->GetLastCommittedOrigin());
ASSERT_EQ(
permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnClick);
@@ -420,7 +438,10 @@
// on all sites -> on click (revokes access)
BlockedActionWaiter blocked_action_waiter(active_action_runner());
permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnClick);
+ UserSiteAccess::kOnClick,
+ GetActiveWebContents()
+ ->GetPrimaryMainFrame()
+ ->GetLastCommittedOrigin());
ASSERT_EQ(
permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnClick);
@@ -442,8 +463,9 @@
// on click -> on site (grants site access and redundantly active tab
// permission)
- permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnSite);
+ permissions_helper_->UpdateSiteAccess(
+ *extension_, GetActiveWebContents(), UserSiteAccess::kOnSite,
+ GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
ASSERT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnSite);
ASSERT_EQ(permissions_helper_->GetSiteInteraction(*extension_,
@@ -457,7 +479,10 @@
// permissions)
BlockedActionWaiter blocked_action_waiter(active_action_runner());
permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnClick);
+ UserSiteAccess::kOnClick,
+ GetActiveWebContents()
+ ->GetPrimaryMainFrame()
+ ->GetLastCommittedOrigin());
ASSERT_EQ(
permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnClick);
@@ -596,8 +621,9 @@
ReloadPageDialogController::AcceptDialogForTesting(true);
// on all sites -> on click (revokes access)
- permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnClick);
+ permissions_helper_->UpdateSiteAccess(
+ *extension_, GetActiveWebContents(), UserSiteAccess::kOnClick,
+ GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
ASSERT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnClick);
ASSERT_EQ(permissions_helper_->GetSiteInteraction(*extension_,
@@ -609,8 +635,9 @@
ExtensionTestMessageListener listener("injection succeeded");
// on click -> on site (grants site access and active tab permission)
- permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnSite);
+ permissions_helper_->UpdateSiteAccess(
+ *extension_, GetActiveWebContents(), UserSiteAccess::kOnSite,
+ GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
ASSERT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnSite);
ASSERT_EQ(permissions_helper_->GetSiteInteraction(*extension_,
@@ -621,8 +648,9 @@
ASSERT_FALSE(ExtensionWantsToRun());
// on site -> on-click (should remove site access and active tab permissions)
- permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnClick);
+ permissions_helper_->UpdateSiteAccess(
+ *extension_, GetActiveWebContents(), UserSiteAccess::kOnClick,
+ GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
EXPECT_EQ(permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnClick);
EXPECT_EQ(permissions_helper_->GetSiteInteraction(*extension_,
@@ -696,7 +724,10 @@
// on all sites -> on site.
ExtensionTestMessageListener listener("success");
permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnSite);
+ UserSiteAccess::kOnSite,
+ GetActiveWebContents()
+ ->GetPrimaryMainFrame()
+ ->GetLastCommittedOrigin());
EXPECT_EQ(
permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnSite);
@@ -711,7 +742,10 @@
// on site -> on-click (refresh needed due to revoking permissions).
BlockedActionWaiter blocked_action_waiter(active_action_runner());
permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnClick);
+ UserSiteAccess::kOnClick,
+ GetActiveWebContents()
+ ->GetPrimaryMainFrame()
+ ->GetLastCommittedOrigin());
EXPECT_EQ(
permissions_manager_->GetUserSiteAccess(*extension_, original_url_),
UserSiteAccess::kOnClick);
@@ -725,7 +759,10 @@
// on click -> on site
ExtensionTestMessageListener listener("success");
permissions_helper_->UpdateSiteAccess(*extension_, GetActiveWebContents(),
- UserSiteAccess::kOnSite);
+ UserSiteAccess::kOnSite,
... (truncated)
Original Bug Report
Potential persistent site access grant TOCTOU in Extensions menu toggle
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A Time-of-Check Time-of-Use (TOCTOU) vulnerability in the Extensions menu allows an attacker to potentially trick a user into granting persistent site permissions to a malicious extension for a sensitive origin. This occurs because the target origin is resolved from the active WebContents at the time of the click rather than being bound to the origin displayed when the menu was rendered.
Affected files:
chrome/browser/ui/extensions/extensions_menu_view_model.ccextensions/browser/permissions/site_permissions_helper.ccchrome/browser/ui/views/extensions/extensions_menu_delegate_desktop.ccchrome/browser/ui/android/extensions/extensions_menu_delegate_android.cc
Estimated timestamp from git blame: 2023-05-14
Summary
A potential Time-of-Check Time-of-Use (TOCTOU) vulnerability exists in the Extensions menu’s site-access management logic (puzzle-piece icon). When a user interacts with the site-access toggle for an extension, the origin for which access is granted is determined by querying the active WebContents at the moment of the click. Because the Extensions menu remains open during page navigations and updates in-place, an attacker can potentially race a navigation to a sensitive origin to intercept the permission grant.
Vulnerability Details
In ExtensionsMenuViewModel::GrantSiteAccess (and similarly in RevokeSiteAccess), the target URL is retrieved from the active WebContents using GetLastCommittedURL():
// chrome/browser/ui/extensions/extensions_menu_view_model.cc
void ExtensionsMenuViewModel::GrantSiteAccess(const extensions::ExtensionId& extension_id) {
...
content::WebContents* web_contents = GetActiveWebContents();
auto url = web_contents->GetLastCommittedURL(); // Determined at click-time
...
SitePermissionsHelper permissions_helper(profile);
permissions_helper.UpdateSiteAccess(*extension, web_contents, new_site_access);
}
The UpdateSiteAccess method in extensions/browser/permissions/site_permissions_helper.cc also re-queries web_contents->GetLastCommittedURL() to perform the actual permission modification.
The Extensions menu does not close automatically when a navigation occurs. Instead, it observes DidFinishNavigation and triggers a UI update via ExtensionsMenuDelegateDesktop::OnPageNavigation to reflect the state of the new page. However, there is no validation that the origin displayed to the user when they initiated the click matches the origin for which the permission is ultimately granted.
Potential Attack Scenario
- A user installs a malicious extension that requests broad host permissions (e.g.,
<all_urls>) but currently has those permissions withheld (Runtime Host Permissions enabled). - The user visits an attacker-controlled site (e.g.,
https://attacker.example). - The user opens the Extensions menu. The menu correctly shows the extension’s site access as “Off” for the current origin.
- The attacker page detects the menu interaction (e.g., via
window.blur) and initiates a top-level navigation to a sensitive site (e.g.,https://victim.example). - The user clicks the toggle to grant site access.
- If the navigation to
victim.examplecommits on the UI thread just before the click event is processed byGrantSiteAccess, the code will read the new sensitive URL and grant the extension persistent host permissions forvictim.exampleinstead ofattacker.example.
Impact
An attacker can obtain durable host permissions for a sensitive origin without informed user consent. This allows a malicious extension to inject content scripts, read cookies, and access sensitive data on the victim origin indefinitely.
Suggested Fix
The Extensions menu should bind the site-access action to the origin that was active when the UI was rendered or when the user initiated the click. ExtensionsMenuViewModel::GrantSiteAccess should accept an url::Origin parameter representing the intended target, and the implementation should verify that this origin still matches the current state of the WebContents before proceeding with the grant.
Note: These steps are suggested based on source code analysis; our current environment does not support running a functional Proof of Concept.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
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.