Chrome · Extensions
CVE-2026-87591
Logic Error in Extensions
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/extensions/api/cookies/cookies_api.cc |
modified | |
ifchrome/browser/extensions/api/cookies/cookies_unittest.cc |
modified | |
TEST_Fchrome/browser/extensions/api/cookies/cookies_unittest.cc |
modified |
Files Changed
chrome/browser/extensions/api/cookies/cookies_api.ccchrome/browser/extensions/api/cookies/cookies_helpers.ccchrome/browser/extensions/api/cookies/cookies_unittest.cc
Patch
From 6813284e59b397be04f5aadd5f4b9ffe87edddff Mon Sep 17 00:00:00 2001 From: Eva Su <[email protected]> Date: Fri, 14 Aug 2026 18:17:01 -0700 Subject: [PATCH] [Extensions] Enforce user site restrictions in chrome.cookies API Previously, the chrome.cookies API verified host access permissions using PermissionsData::HasHostPermission(url) which checks explicit manifest host permissions and enterprise policy host blocks, but does not check user host restrictions or withheld permissions. So when a user explicitly turns off extensions for a site or withholds host permissions, an extension with host permissions could continue using chrome.cookies methods to read, create, and remove cookies for that domain. This CL replaces the HasHostPermission check with GetPageAccess which is a superset of what HasHostPermission checks that also validates user site restrictions, withheld permissions, enterprise policy, and restricted URLs. Fixed: 543938457 Change-Id: Ic46119d4da0bb32b84c3c3e90eec696c110a2e16 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8259390 Reviewed-by: Solomon Kinard <[email protected]> Commit-Queue: Eva Su <[email protected]> Cr-Commit-Position: refs/heads/main@{#1680073} --- diff --git a/chrome/browser/extensions/api/cookies/cookies_api.cc b/chrome/browser/extensions/api/cookies/cookies_api.cc index b605d852..46d1698f 100644 --- a/chrome/browser/extensions/api/cookies/cookies_api.cc +++ b/chrome/browser/extensions/api/cookies/cookies_api.cc @@ -28,6 +28,7 @@ #include "extensions/browser/event_router.h" #include "extensions/browser/extension_api_frame_id_map.h" #include "extensions/browser/extensions_browser_client.h" +#include "extensions/common/constants.h" #include "extensions/common/error_utils.h" #include "extensions/common/extension.h" #include "extensions/common/permissions/permissions_data.h" @@ -74,9 +75,16 @@ bool CheckHostPermissions(const Extension* extension, const GURL& url, std::string* error) { - if (!extension->permissions_data()->HasHostPermission(url)) { - *error = - ErrorUtils::FormatErrorMessage(kNoHostPermissionsError, url.spec()); + // Cookie operations are profile-scoped and not tied to a specific tab + // context so we pass kUnknownTabId to check page access without considering + // tab-specific grants. + if (extension->permissions_data()->GetPageAccess( + url, extension_misc::kUnknownTabId, /*error=*/nullptr) != + PermissionsData::PageAccess::kAllowed) { + if (error) { + *error = + ErrorUtils::FormatErrorMessage(kNoHostPermissionsError, url.spec()); + } return false; } return true; diff --git a/chrome/browser/extensions/api/cookies/cookies_helpers.cc b/chrome/browser/extensions/api/cookies/cookies_helpers.cc index 911552d..fa5319f 100644 --- a/chrome/browser/extensions/api/cookies/cookies_helpers.cc +++ b/chrome/browser/extensions/api/cookies/cookies_helpers.cc @@ -22,6 +22,7 @@ #include "chrome/browser/profiles/profile.h" #include "chrome/common/extensions/api/cookies.h" #include "content/public/browser/web_contents.h" +#include "extensions/common/constants.h" #include "extensions/common/extension.h" #include "extensions/common/permissions/permissions_data.h" #include "mojo/public/cpp/bindings/callback_helpers.h" @@ -47,10 +48,13 @@ const Extension* extension, std::vector<Cookie>* match_vector, const net::CookiePartitionKeyCollection& cookie_partition_key_collection) { - // Ignore any cookie whose domain doesn't match the extension's - // host permissions. + // Ignore any cookie whose domain doesn't match the extension's page access + // controls. Pass kUnknownTabId because cookie operations are profile-wide + // rather than tab-bound. GURL cookie_domain_url = cookies_helpers::GetURLFromCanonicalCookie(cookie); - if (!extension->permissions_data()->HasHostPermission(cookie_domain_url)) { + if (extension->permissions_data()->GetPageAccess( + cookie_domain_url, extension_misc::kUnknownTabId, nullptr) != + PermissionsData::PageAccess::kAllowed) { return; } // Filter the cookie using the match filter. diff --git a/chrome/browser/extensions/api/cookies/cookies_unittest.cc b/chrome/browser/extensions/api/cookies/cookies_unittest.cc index 2f8a254..526d102 100644 --- a/chrome/browser/extensions/api/cookies/cookies_unittest.cc +++ b/chrome/browser/extensions/api/cookies/cookies_unittest.cc @@ -15,6 +15,7 @@ #include <vector> #include "base/test/gtest_util.h" +#include "base/test/scoped_feature_list.h" #include "base/values.h" #include "chrome/browser/extensions/api/cookies/cookies_helpers.h" #include "chrome/common/extensions/api/cookies.h" @@ -22,6 +23,9 @@ #include "chrome/test/base/testing_profile.h" #include "content/public/test/browser_task_environment.h" #include "extensions/buildflags/buildflags.h" +#include "extensions/common/extension_builder.h" +#include "extensions/common/extension_features.h" +#include "extensions/common/permissions/permissions_data.h" #include "net/cookies/canonical_cookie.h" #include "net/cookies/cookie_constants.h" #include "testing/gtest/include/gtest/gtest.h" @@ -305,4 +309,88 @@ EXPECT_CHECK_DEATH(cookies_helpers::CreateCookie(*opaque_cookie, "0")); } +namespace { + +std::vector<Cookie> GetMatchingCookiesForExtension( + const Extension* extension, + const std::string& domain = "example.com") { + auto cookie = net::CanonicalCookie::CreateUnsafeCookieForTesting( + "ABC", "DEF", domain, "/", base::Time(), base::Time(), base::Time(), + base::Time(), false, false, net::CookieSameSite::NO_RESTRICTION, + net::COOKIE_PRIORITY_DEFAULT, net::CookieSourceType::kOther); + if (!cookie) { + return {}; + } + + base::DictValue dict; + dict.Set("storeId", "0"); + auto details = GetAll::Params::Details::FromValue(dict); + if (!details) { + return {}; + } + + std::vector<Cookie> match_vector; + cookies_helpers::AppendMatchingCookiesFromCookieListToVector( + {*cookie}, &details.value(), extension, &match_vector, + net::CookiePartitionKeyCollection()); + return match_vector; +} + +} // namespace + +// Ensures cookies from domains explicitly blocked by the user are excluded from +// matches. +TEST_F(ExtensionCookiesTest, AppendMatchingCookiesWithUserBlockedSite) { + base::test::ScopedFeatureList feature_list( + extensions_features::kExtensionsMenuAccessControl); + + scoped_refptr<const Extension> extension = + ExtensionBuilder("Test Extension") + .AddHostPermission("<all_urls>") + .Build(); + + constexpr int kContextId = 1; + extension->permissions_data()->SetContextId(kContextId); + + // Mark example.com as user-blocked. + URLPatternSet user_blocked_hosts; + URLPattern pattern(URLPattern::SCHEME_ALL, "*://*.example.com/*"); + user_blocked_hosts.AddPattern(pattern); + PermissionsData::SetUserHostRestrictions(kContextId, + std::move(user_blocked_hosts), + /*user_allowed_hosts=*/{}); + + // Since example.com is user-blocked, the cookie must not be matched. + EXPECT_TRUE(GetMatchingCookiesForExtension(extension.get()).empty()); +} + +// Ensures cookies from domains with withheld host permissions are excluded from +// matches. +TEST_F(ExtensionCookiesTest, AppendMatchingCookiesWithWithheldPermissions) { + base::test::ScopedFeatureList feature_list( + extensions_features::kExtensionsMenuAccessControl); + + base::ListValue host_permissions; + host_permissions.Append("*://*.example.com/*"); + + scoped_refptr<const Extension> extension = + ExtensionBuilder("Test Extension") + .SetManifestKey("host_permissions", std::move(host_permissions)) + .Build(); + + // Withhold the host permission from the extension. + URLPatternSet withheld_hosts; + withheld_hosts.AddPattern( + URLPattern(URLPattern::SCHEME_ALL, "*://*.example.com/*")); + + extension->permissions_data()->SetPermissions( + std::make_unique<PermissionSet>(), + std::make_unique<PermissionSet>(APIPermissionSet(), + ManifestPermissionSet(), + withheld_hosts.Clone(), URLPatternSet())); + + // Since the permission is withheld, the cookie must not be matched. + EXPECT_TRUE(GetMatchingCookiesForExtension(extension.get()).empty()); +} + } // namespace extensions
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/extensions/api/cookies/cookies_unittest.cc b/chrome/browser/extensions/api/cookies/cookies_unittest.cc
index 2f8a254..526d102 100644
--- a/chrome/browser/extensions/api/cookies/cookies_unittest.cc
+++ b/chrome/browser/extensions/api/cookies/cookies_unittest.cc
@@ -15,6 +15,7 @@
#include <vector>
#include "base/test/gtest_util.h"
+#include "base/test/scoped_feature_list.h"
#include "base/values.h"
#include "chrome/browser/extensions/api/cookies/cookies_helpers.h"
#include "chrome/common/extensions/api/cookies.h"
@@ -22,6 +23,9 @@
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/browser_task_environment.h"
#include "extensions/buildflags/buildflags.h"
+#include "extensions/common/extension_builder.h"
+#include "extensions/common/extension_features.h"
+#include "extensions/common/permissions/permissions_data.h"
#include "net/cookies/canonical_cookie.h"
#include "net/cookies/cookie_constants.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -305,4 +309,88 @@
EXPECT_CHECK_DEATH(cookies_helpers::CreateCookie(*opaque_cookie, "0"));
}
+namespace {
+
+std::vector<Cookie> GetMatchingCookiesForExtension(
+ const Extension* extension,
+ const std::string& domain = "example.com") {
+ auto cookie = net::CanonicalCookie::CreateUnsafeCookieForTesting(
+ "ABC", "DEF", domain, "/", base::Time(), base::Time(), base::Time(),
+ base::Time(), false, false, net::CookieSameSite::NO_RESTRICTION,
+ net::COOKIE_PRIORITY_DEFAULT, net::CookieSourceType::kOther);
+ if (!cookie) {
+ return {};
+ }
+
+ base::DictValue dict;
+ dict.Set("storeId", "0");
+ auto details = GetAll::Params::Details::FromValue(dict);
+ if (!details) {
+ return {};
+ }
+
+ std::vector<Cookie> match_vector;
+ cookies_helpers::AppendMatchingCookiesFromCookieListToVector(
+ {*cookie}, &details.value(), extension, &match_vector,
+ net::CookiePartitionKeyCollection());
+ return match_vector;
+}
+
+} // namespace
+
+// Ensures cookies from domains explicitly blocked by the user are excluded from
+// matches.
+TEST_F(ExtensionCookiesTest, AppendMatchingCookiesWithUserBlockedSite) {
+ base::test::ScopedFeatureList feature_list(
+ extensions_features::kExtensionsMenuAccessControl);
+
+ scoped_refptr<const Extension> extension =
+ ExtensionBuilder("Test Extension")
+ .AddHostPermission("<all_urls>")
+ .Build();
+
+ constexpr int kContextId = 1;
+ extension->permissions_data()->SetContextId(kContextId);
+
+ // Mark example.com as user-blocked.
+ URLPatternSet user_blocked_hosts;
+ URLPattern pattern(URLPattern::SCHEME_ALL, "*://*.example.com/*");
+ user_blocked_hosts.AddPattern(pattern);
+ PermissionsData::SetUserHostRestrictions(kContextId,
+ std::move(user_blocked_hosts),
+ /*user_allowed_hosts=*/{});
+
+ // Since example.com is user-blocked, the cookie must not be matched.
+ EXPECT_TRUE(GetMatchingCookiesForExtension(extension.get()).empty());
+}
+
+// Ensures cookies from domains with withheld host permissions are excluded from
+// matches.
+TEST_F(ExtensionCookiesTest, AppendMatchingCookiesWithWithheldPermissions) {
+ base::test::ScopedFeatureList feature_list(
+ extensions_features::kExtensionsMenuAccessControl);
+
+ base::ListValue host_permissions;
+ host_permissions.Append("*://*.example.com/*");
+
+ scoped_refptr<const Extension> extension =
+ ExtensionBuilder("Test Extension")
+ .SetManifestKey("host_permissions", std::move(host_permissions))
+ .Build();
+
+ // Withhold the host permission from the extension.
+ URLPatternSet withheld_hosts;
+ withheld_hosts.AddPattern(
+ URLPattern(URLPattern::SCHEME_ALL, "*://*.example.com/*"));
+
+ extension->permissions_data()->SetPermissions(
+ std::make_unique<PermissionSet>(),
+ std::make_unique<PermissionSet>(APIPermissionSet(),
+ ManifestPermissionSet(),
+ withheld_hosts.Clone(), URLPatternSet()));
+
+ // Since the permission is withheld, the cookie must not be matched.
+ EXPECT_TRUE(GetMatchingCookiesForExtension(extension.get()).empty());
+}
+
} // namespace extensions
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