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 <evasu@chromium.org>
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 <solomonkinard@chromium.org>
Commit-Queue: Eva Su <evasu@chromium.org>
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