CVE-2026-14022
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifservices/network/restricted_cookie_manager.cc |
modified | |
TEST_Pservices/network/restricted_cookie_manager_unittest.cc |
modified |
Files Changed
services/network/restricted_cookie_manager.ccservices/network/restricted_cookie_manager_unittest.cc
Patch
From a7f495cb392d1978db914e87e0f92d2fb4b46251 Mon Sep 17 00:00:00 2001 From: Chris Fredrickson <[email protected]> Date: Fri, 29 May 2026 14:14:27 -0700 Subject: [PATCH] Add explicit validation at start of GetCookiesString I've also audited all of the methods in the mojo interface, and made sure that they all call ValidateAccessToCookiesAt before doing anything else (with the exception of AddChangeListener, which silently returns for calls whose `url` is empty). Fixed: 517791835 Change-Id: Iefbbc79db8ed82a0e541717627cab285d6afe02c Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7882551 Commit-Queue: Chris Fredrickson <[email protected]> Commit-Queue: mmenke <[email protected]> Auto-Submit: Chris Fredrickson <[email protected]> Reviewed-by: mmenke <[email protected]> Cr-Commit-Position: refs/heads/main@{#1638725} --- diff --git a/services/network/restricted_cookie_manager.cc b/services/network/restricted_cookie_manager.cc index 1c9e46f..b810301 100644 --- a/services/network/restricted_cookie_manager.cc +++ b/services/network/restricted_cookie_manager.cc @@ -1025,8 +1025,13 @@ GetCookiesStringCallback callback) { TRACE_EVENT("net", "RestrictedCookieManager::GetCookiesString"); DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (!ValidateAccessToCookiesAt(url, site_for_cookies, top_frame_origin)) { + std::move(callback).Run(mojo::shared_memory_version::kInvalidVersion, {}, + ""); + return; + } + base::ElapsedTimer timer; - // Checks done by GetAllForUrl if (metrics_updater_) { metrics_updater_->OnGetCookiesString(); diff --git a/services/network/restricted_cookie_manager_unittest.cc b/services/network/restricted_cookie_manager_unittest.cc index 553bd17d..6231e5b 100644 --- a/services/network/restricted_cookie_manager_unittest.cc +++ b/services/network/restricted_cookie_manager_unittest.cc @@ -852,7 +852,7 @@ } } -TEST_P(RestrictedCookieManagerTest, GetCookieStringFromWrongOrigin) { +TEST_P(RestrictedCookieManagerTest, GetCookiesStringFromWrongOrigin) { SetSessionCookie("cookie-name", "cookie-value", "example.com", "/"); SetSessionCookie("cookie-name-2", "cookie-value-2", "example.com", "/"); SetSessionCookie("other-cookie-name", "other-cookie-value", "notexample.com", @@ -872,6 +872,8 @@ &cookies_out)); EXPECT_TRUE(received_bad_message()); EXPECT_THAT(cookies_out, IsEmpty()); + EXPECT_EQ(version, mojo::shared_memory_version::kInvalidVersion); + EXPECT_FALSE(mapped_region.IsValid()); // One more time but also requesting some shared memory. EXPECT_TRUE(backend()->GetCookiesString( @@ -883,6 +885,31 @@ &cookies_out)); EXPECT_TRUE(received_bad_message()); EXPECT_THAT(cookies_out, IsEmpty()); + EXPECT_EQ(version, mojo::shared_memory_version::kInvalidVersion); + EXPECT_FALSE(mapped_region.IsValid()); +} + +TEST_P(RestrictedCookieManagerTest, GetCookiesStringFromOpaqueOrigin) { + SetSessionCookie("cookie-name", "cookie-value", "example.com", "/"); + + service_->OverrideOriginForTesting(url::Origin()); + + ExpectBadMessage("Access is denied in this context"); + std::string cookies_out; + base::ReadOnlySharedMemoryRegion mapped_region; + uint64_t version = 0; + + EXPECT_TRUE(backend()->GetCookiesString( + kDefaultUrlWithPath, kDefaultSiteForCookies, kDefaultOrigin, + net::StorageAccessApiStatus::kNone, + /*get_version_shared_memory=*/true, + /*is_ad_tagged=*/false, /*apply_devtools_overrides=*/false, + /*force_disable_third_party_cookies=*/false, &version, &mapped_region, + &cookies_out)); + EXPECT_TRUE(received_bad_message()); + EXPECT_EQ(cookies_out, ""); + EXPECT_EQ(version, mojo::shared_memory_version::kInvalidVersion); + EXPECT_FALSE(mapped_region.IsValid()); } TEST_P(RestrictedCookieManagerTest, GetAllAdTagged) {
Regression Test / PoC
diff --git a/services/network/restricted_cookie_manager_unittest.cc b/services/network/restricted_cookie_manager_unittest.cc
index 553bd17d..6231e5b 100644
--- a/services/network/restricted_cookie_manager_unittest.cc
+++ b/services/network/restricted_cookie_manager_unittest.cc
@@ -852,7 +852,7 @@
}
}
-TEST_P(RestrictedCookieManagerTest, GetCookieStringFromWrongOrigin) {
+TEST_P(RestrictedCookieManagerTest, GetCookiesStringFromWrongOrigin) {
SetSessionCookie("cookie-name", "cookie-value", "example.com", "/");
SetSessionCookie("cookie-name-2", "cookie-value-2", "example.com", "/");
SetSessionCookie("other-cookie-name", "other-cookie-value", "notexample.com",
@@ -872,6 +872,8 @@
&cookies_out));
EXPECT_TRUE(received_bad_message());
EXPECT_THAT(cookies_out, IsEmpty());
+ EXPECT_EQ(version, mojo::shared_memory_version::kInvalidVersion);
+ EXPECT_FALSE(mapped_region.IsValid());
// One more time but also requesting some shared memory.
EXPECT_TRUE(backend()->GetCookiesString(
@@ -883,6 +885,31 @@
&cookies_out));
EXPECT_TRUE(received_bad_message());
EXPECT_THAT(cookies_out, IsEmpty());
+ EXPECT_EQ(version, mojo::shared_memory_version::kInvalidVersion);
+ EXPECT_FALSE(mapped_region.IsValid());
+}
+
+TEST_P(RestrictedCookieManagerTest, GetCookiesStringFromOpaqueOrigin) {
+ SetSessionCookie("cookie-name", "cookie-value", "example.com", "/");
+
+ service_->OverrideOriginForTesting(url::Origin());
+
+ ExpectBadMessage("Access is denied in this context");
+ std::string cookies_out;
+ base::ReadOnlySharedMemoryRegion mapped_region;
+ uint64_t version = 0;
+
+ EXPECT_TRUE(backend()->GetCookiesString(
+ kDefaultUrlWithPath, kDefaultSiteForCookies, kDefaultOrigin,
+ net::StorageAccessApiStatus::kNone,
+ /*get_version_shared_memory=*/true,
+ /*is_ad_tagged=*/false, /*apply_devtools_overrides=*/false,
+ /*force_disable_third_party_cookies=*/false, &version, &mapped_region,
+ &cookies_out));
+ EXPECT_TRUE(received_bad_message());
+ EXPECT_EQ(cookies_out, "");
+ EXPECT_EQ(version, mojo::shared_memory_version::kInvalidVersion);
+ EXPECT_FALSE(mapped_region.IsValid());
}
TEST_P(RestrictedCookieManagerTest, GetAllAdTagged) {
Original Bug Report
Persistent cross-origin cookie-write oracle via RestrictedCookieManager leak
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 potential validation-after-side-effect in RestrictedCookieManager allows a compromised renderer to swap its cookie subscription to a victim origin before validation occurs. On official builds, bad message reporting on point-to-point peer channels does not terminate the renderer and bypasses the local disconnect handler, leaking the manager instance. This allows the surviving compromised renderer to persistently monitor victim cookie writes via shared memory.
Affected files:
services/network/restricted_cookie_manager.ccservices/network/network_context.ccservices/network/network_service.cc
Estimated timestamp from git blame: 2023-08-17
Summary
A potential validation-after-side-effect and Mojo object lifecycle leak in RestrictedCookieManager allows a compromised renderer to establish a persistent, cross-origin cookie-write oracle. Because origin validation is executed after the cookie subscription is updated to a target URL, and because bad message reporting on introduced peer channels does not terminate the renderer in official builds while leaking the manager instance, a compromised renderer can continuously monitor unpartitioned cookie writes to arbitrary origins (including HttpOnly cookies) via shared memory.
Vulnerability Mechanism
1. Validation-After-Side-Effect in GetCookiesString
In services/network/restricted_cookie_manager.cc, RestrictedCookieManager::GetCookiesString processes requests as follows:
base::ReadOnlySharedMemoryRegion shared_memory_region;
if (get_version_shared_memory) {
shared_memory_region = GetAndPrepareSharedMemoryRegion(url);
}
// ...
GetAllForUrl(url, ...);
GetAndPrepareSharedMemoryRegion(url) is invoked with the unvalidated, renderer-supplied url before GetAllForUrl performs origin validation via ValidateAccessToCookiesAt.
Inside GetAndPrepareSharedMemoryRegion(url):
bool new_url = cookie_store_subscription_ && change_subscribed_url_ != url;
if (!cookie_store_subscription_ || new_url) {
change_subscribed_url_ = url;
cookie_store_subscription_ =
cookie_store_->GetChangeDispatcher().AddCallbackForUrl(
url, cookie_partition_key_, ...);
}
If the renderer requests a new URL (e.g., https://victim.com), the subscription cookie_store_subscription_ is immediately updated to monitor cookie changes for the victim URL. Subsequently, GetAllForUrl invokes ValidateAccessToCookiesAt, which correctly detects that the renderer origin is unauthorized to access the victim URL and reports a bad message. However, the side effect—updating the subscription—has already occurred.
2. Renderer Survives Bad Message Termination
Ordinarily, bad messages terminate the sending process. However, the renderer-to-network Mojo channel is a point-to-point peer channel created via broker introduction (mojo/core/node_controller.cc:1280) with an empty ProcessErrorCallback.
As a result, HasBadMessageHandler() returns false, causing Core::NotifyBadMessage to fall back to the default process error handler HandleBadMessage registered in the Network Process (services/network/network_service.cc).
In official builds, HandleBadMessage logs a warning and calls base::debug::DumpWithoutCrashing(), but does not crash or terminate the process. Therefore, the compromised renderer continues running.
3. RestrictedCookieManager Instance is Leaked
When ReportBadMessage is called on the Mojo receiver, Mojo’s bindings layer closes the receiver locally via binding->Close().
Because the receiver endpoint is closed/reset locally, the registered disconnect handler (which is configured as NetworkContext::OnRCMDisconnect) is discarded and not executed. Consequently, the RestrictedCookieManager instance is never removed from NetworkContext::restricted_cookie_managers_ and is permanently leaked.
4. Shared Memory Cookie Write Oracle
The subscription callback to IncrementSharedVersion remains active because the leaked RestrictedCookieManager is kept alive inside the NetworkContext’s owner set:
void RestrictedCookieManager::IncrementSharedVersion() {
shared_memory_version_controller_.Increment();
}
Since the compromised renderer previously performed a legitimate GetCookiesString call on the same interface instance, it retains the read-only shared memory mapping. Any unpartitioned cookie write/update/deletion on the victim origin (including HttpOnly cookies) synchronously triggers the subscription, incrementing the atomic version counter in the mapped region. The compromised renderer can spin-read this memory at CPU speeds to reliably detect victim cookie writes.
Potential Steps to Reproduce
Note: These are suggested steps; our tooling cannot execute code.
- Obtain Shared Memory Mapping: A compromised renderer bound to
https://attacker.comrequests aRestrictedCookieManagerreceiver. It callsGetCookiesStringforhttps://attacker.comwithget_version_shared_memory = true. It maps the receivedReadOnlySharedMemoryRegioninto its address space. - Swap Subscription: The renderer calls
GetCookiesStringagain asynchronously on the same interface, passingurl = "https://victim.com"andget_version_shared_memory = true. - Trigger Validation Failure: In the network process,
GetAndPrepareSharedMemoryRegionswaps the subscription tohttps://victim.com, after which validation fails andReportBadMessageis invoked. - Leak and Monitor: The receiver closes, but the
RestrictedCookieManageris leaked with the victim subscription active, and the renderer is not terminated. The renderer reads the atomic variable in the mapped shared memory. Any increment confirms a cookie modification occurred on the victim origin.
Suggested Fix
Move the origin access check ValidateAccessToCookiesAt to the beginning of RestrictedCookieManager::GetCookiesString before GetAndPrepareSharedMemoryRegion is executed:
void RestrictedCookieManager::GetCookiesString(
const GURL& url,
const net::SiteForCookies& site_for_cookies,
const url::Origin& top_frame_origin,
net::StorageAccessApiStatus storage_access_api_status,
bool get_version_shared_memory,
bool is_ad_tagged,
bool apply_devtools_overrides,
bool force_disable_third_party_cookies,
GetCookiesStringCallback callback) {
TRACE_EVENT("net", "RestrictedCookieManager::GetCookiesString");
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::ElapsedTimer timer;
if (!ValidateAccessToCookiesAt(url, site_for_cookies, top_frame_origin)) {
std::move(callback).Run(mojo::shared_memory_version::kInvalidVersion,
base::ReadOnlySharedMemoryRegion());
return;
}
if (metrics_updater_) {
metrics_updater_->OnGetCookiesString();
}
base::ReadOnlySharedMemoryRegion shared_memory_region;
if (get_version_shared_memory) {
shared_memory_region = GetAndPrepareSharedMemoryRegion(url);
}
// ...
}
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
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.