CVE-2026-79186
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TestNetworkAnnotationMonitorservices/network/network_service_unittest.cc |
modified |
Files Changed
services/network/network_service_network_delegate.ccservices/network/network_service_unittest.cc
Patch
From db1fc539aaee75e109e430b2c3929ff026ae2f9b Mon Sep 17 00:00:00 2001 From: Jiacheng Guo <[email protected]> Date: Mon, 29 Jun 2026 16:47:07 -0700 Subject: [PATCH] Use IsolationInfo's SiteForCookies when handling Clear-Site-Data NetworkServiceNetworkDelegate::HandleClearSiteDataHeader feeds URLRequest::site_for_cookies() into IsPrivacyModeEnabled() to decide whether the resulting OnClearSiteData notification is restricted to partitioned state. The adjacent top_frame_origin argument already comes from the request's IsolationInfo, which is supplied via URLLoaderFactoryParams or trusted_params. Consult the same IsolationInfo for site_for_cookies as well so that both inputs agree and the partitioned-state-only decision matches the embedding context described by the factory even if the per-request site_for_cookies disagrees. Add a unit test that creates a factory whose IsolationInfo describes a cross-site subframe, enables third-party cookie blocking, and issues a request whose site_for_cookies claims a first-party context. It verifies that OnClearSiteData reports partitioned_state_allowed_only=true. Bug: 497646947 Change-Id: Id61ffa5e6f2eec79721f5b5f8557853d433c8976 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8017564 Commit-Queue: Jiacheng Guo <[email protected]> Reviewed-by: Adam Rice <[email protected]> Cr-Commit-Position: refs/heads/main@{#1654466} --- diff --git a/services/network/network_service_network_delegate.cc b/services/network/network_service_network_delegate.cc index fa98a19..1d0afb6 100644 --- a/services/network/network_service_network_delegate.cc +++ b/services/network/network_service_network_delegate.cc @@ -417,7 +417,7 @@ auto& cookie_settings = network_context_->cookie_manager()->cookie_settings(); net::NetworkDelegate::PrivacySetting privacy_settings = cookie_settings.IsPrivacyModeEnabled( - request->url(), request->site_for_cookies(), + request->url(), request->isolation_info().site_for_cookies(), request->isolation_info().top_frame_origin(), request->cookie_setting_overrides()); bool partitioned_state_allowed_only = diff --git a/services/network/network_service_unittest.cc b/services/network/network_service_unittest.cc index 4326564..32435238 100644 --- a/services/network/network_service_unittest.cc +++ b/services/network/network_service_unittest.cc @@ -32,11 +32,13 @@ #include "net/base/features.h" #include "net/base/ip_address.h" #include "net/base/ip_endpoint.h" +#include "net/base/isolation_info.h" #include "net/base/mock_network_change_notifier.h" #include "net/base/url_util.h" #include "net/cookies/canonical_cookie.h" #include "net/cookies/cookie_options.h" #include "net/cookies/cookie_util.h" +#include "net/cookies/site_for_cookies.h" #include "net/dns/dns_client.h" #include "net/dns/dns_config.h" #include "net/dns/dns_config_service.h" @@ -82,6 +84,7 @@ #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "url/gurl.h" +#include "url/origin.h" #if BUILDFLAG(USE_KERBEROS) #include "net/http/http_auth_handler_negotiate.h" @@ -2092,6 +2095,7 @@ OnClearSiteDataCallback callback) override { ++on_clear_site_data_counter_; last_on_clear_site_data_header_value_ = header_value; + last_partitioned_state_allowed_only_ = partitioned_state_allowed_only; std::move(callback).Run(); } @@ -2101,14 +2105,20 @@ return last_on_clear_site_data_header_value_; } + bool last_partitioned_state_allowed_only() const { + return last_partitioned_state_allowed_only_; + } + void ClearOnClearSiteDataCounter() { on_clear_site_data_counter_ = 0; last_on_clear_site_data_header_value_.clear(); + last_partitioned_state_allowed_only_ = false; } private: int on_clear_site_data_counter_ = 0; std::string last_on_clear_site_data_header_value_; + bool last_partitioned_state_allowed_only_ = false; }; // Check that |NetworkServiceNetworkDelegate| handles Clear-Site-Data header @@ -2194,6 +2204,59 @@ } } +// When third-party cookies are blocked and a Clear-Site-Data response is +// received for a request issued from a cross-site subframe, the network +// delegate must report that only partitioned state may be cleared. This must +// hold even if the request's site_for_cookies disagrees with the factory's +// IsolationInfo. +TEST_F(NetworkServiceNetworkDelegateTest, + ClearSiteDataPartitionedStateOnlyForCrossSiteSubframe) { + const char kClearCookiesHeader[] = "Clear-Site-Data: \"cookies\""; + + mojom::NetworkContextParamsPtr context_params = + mojom::NetworkContextParams::New(); + context_params->cookie_manager_params = mojom::CookieManagerParams::New(); + context_params->cookie_manager_params->block_third_party_cookies = true; + CreateNetworkContext(std::move(context_params)); + + ClearSiteDataAuthCertObserver clear_site_observer; + + GURL url = https_server()->GetURL("/foo"); + url = AddQuery(url, "header", kClearCookiesHeader); + const url::Origin top_frame_origin = url::Origin::Create(url); + const url::Origin frame_origin = + url::Origin::Create(GURL("https://other-site.test")); + + mojo::Remote<mojom::URLLoaderFactory> loader_factory; + mojom::URLLoaderFactoryParamsPtr params = + mojom::URLLoaderFactoryParams::New(); + params->process_id = OriginatingProcessId::browser(); + params->is_orb_enabled = false; + params->isolation_info = net::IsolationInfo::Create( + net::IsolationInfo::RequestType::kOther, top_frame_origin, frame_origin, + net::SiteForCookies()); + params->url_loader_network_observer = clear_site_observer.Bind(); + network_context_->CreateURLLoaderFactory( + loader_factory.BindNewPipeAndPassReceiver(), std::move(params)); + + ResourceRequest request; + request.url = url; + request.method = "GET"; + request.request_initiator = frame_origin; + request.site_for_cookies = net::SiteForCookies::FromOrigin(top_frame_origin); + + client_ = std::make_unique<TestURLLoaderClient>(); + loader_.reset(); + loader_factory->CreateLoaderAndStart( + loader_.BindNewPipeAndPassReceiver(), 1, mojom::kURLLoadOptionNone, + request, client_->CreateRemote(), + net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS)); + client_->RunUntilComplete(); + + EXPECT_EQ(1, clear_site_observer.on_clear_site_data_counter()); + EXPECT_TRUE(clear_site_observer.last_partitioned_state_allowed_only()); +} + class TestNetworkAnnotationMonitor : public mojom::NetworkAnnotationMonitor { public: mojo::PendingRemote<mojom::NetworkAnnotationMonitor> GetClient() {
Regression Test / PoC
diff --git a/services/network/network_service_unittest.cc b/services/network/network_service_unittest.cc
index 4326564..32435238 100644
--- a/services/network/network_service_unittest.cc
+++ b/services/network/network_service_unittest.cc
@@ -32,11 +32,13 @@
#include "net/base/features.h"
#include "net/base/ip_address.h"
#include "net/base/ip_endpoint.h"
+#include "net/base/isolation_info.h"
#include "net/base/mock_network_change_notifier.h"
#include "net/base/url_util.h"
#include "net/cookies/canonical_cookie.h"
#include "net/cookies/cookie_options.h"
#include "net/cookies/cookie_util.h"
+#include "net/cookies/site_for_cookies.h"
#include "net/dns/dns_client.h"
#include "net/dns/dns_config.h"
#include "net/dns/dns_config_service.h"
@@ -82,6 +84,7 @@
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
+#include "url/origin.h"
#if BUILDFLAG(USE_KERBEROS)
#include "net/http/http_auth_handler_negotiate.h"
@@ -2092,6 +2095,7 @@
OnClearSiteDataCallback callback) override {
++on_clear_site_data_counter_;
last_on_clear_site_data_header_value_ = header_value;
+ last_partitioned_state_allowed_only_ = partitioned_state_allowed_only;
std::move(callback).Run();
}
@@ -2101,14 +2105,20 @@
return last_on_clear_site_data_header_value_;
}
+ bool last_partitioned_state_allowed_only() const {
+ return last_partitioned_state_allowed_only_;
+ }
+
void ClearOnClearSiteDataCounter() {
on_clear_site_data_counter_ = 0;
last_on_clear_site_data_header_value_.clear();
+ last_partitioned_state_allowed_only_ = false;
}
private:
int on_clear_site_data_counter_ = 0;
std::string last_on_clear_site_data_header_value_;
+ bool last_partitioned_state_allowed_only_ = false;
};
// Check that |NetworkServiceNetworkDelegate| handles Clear-Site-Data header
@@ -2194,6 +2204,59 @@
}
}
+// When third-party cookies are blocked and a Clear-Site-Data response is
+// received for a request issued from a cross-site subframe, the network
+// delegate must report that only partitioned state may be cleared. This must
+// hold even if the request's site_for_cookies disagrees with the factory's
+// IsolationInfo.
+TEST_F(NetworkServiceNetworkDelegateTest,
+ ClearSiteDataPartitionedStateOnlyForCrossSiteSubframe) {
+ const char kClearCookiesHeader[] = "Clear-Site-Data: \"cookies\"";
+
+ mojom::NetworkContextParamsPtr context_params =
+ mojom::NetworkContextParams::New();
+ context_params->cookie_manager_params = mojom::CookieManagerParams::New();
+ context_params->cookie_manager_params->block_third_party_cookies = true;
+ CreateNetworkContext(std::move(context_params));
+
+ ClearSiteDataAuthCertObserver clear_site_observer;
+
+ GURL url = https_server()->GetURL("/foo");
+ url = AddQuery(url, "header", kClearCookiesHeader);
+ const url::Origin top_frame_origin = url::Origin::Create(url);
+ const url::Origin frame_origin =
+ url::Origin::Create(GURL("https://other-site.test"));
+
+ mojo::Remote<mojom::URLLoaderFactory> loader_factory;
+ mojom::URLLoaderFactoryParamsPtr params =
+ mojom::URLLoaderFactoryParams::New();
+ params->process_id = OriginatingProcessId::browser();
+ params->is_orb_enabled = false;
+ params->isolation_info = net::IsolationInfo::Create(
+ net::IsolationInfo::RequestType::kOther, top_frame_origin, frame_origin,
+ net::SiteForCookies());
+ params->url_loader_network_observer = clear_site_observer.Bind();
+ network_context_->CreateURLLoaderFactory(
+ loader_factory.BindNewPipeAndPassReceiver(), std::move(params));
+
+ ResourceRequest request;
+ request.url = url;
+ request.method = "GET";
+ request.request_initiator = frame_origin;
+ request.site_for_cookies = net::SiteForCookies::FromOrigin(top_frame_origin);
+
+ client_ = std::make_unique<TestURLLoaderClient>();
+ loader_.reset();
+ loader_factory->CreateLoaderAndStart(
+ loader_.BindNewPipeAndPassReceiver(), 1, mojom::kURLLoadOptionNone,
+ request, client_->CreateRemote(),
+ net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+ client_->RunUntilComplete();
+
+ EXPECT_EQ(1, clear_site_observer.on_clear_site_data_counter());
+ EXPECT_TRUE(clear_site_observer.last_partitioned_state_allowed_only());
+}
+
class TestNetworkAnnotationMonitor : public mojom::NetworkAnnotationMonitor {
public:
mojo::PendingRemote<mojom::NetworkAnnotationMonitor> GetClient() {
Original Bug Report
Site Isolation Bypass via forged site_for_cookies in Clear-Site-Data
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A compromised renderer can forge the site_for_cookies field in a cross-origin subresource request to bypass third-party cookie restrictions. This allows an attacker to trigger the Clear-Site-Data: "cookies" mechanism and delete unpartitioned, first-party cookies (including HttpOnly session cookies) of any target site.
Affected files:
services/network/network_service_network_delegate.cccomponents/content_settings/core/common/cookie_settings_base.ccservices/network/cors/cors_url_loader_factory.cccontent/browser/storage_partition_impl.cccontent/browser/browsing_data/clear_site_data_utils.ccnet/cookies/cookie_deletion_info.cc
Estimated timestamp from git blame: 2024-10-24
Summary
A potential vulnerability exists in how the Network Service handles the Clear-Site-Data HTTP response header. A compromised renderer can forge the site_for_cookies attribute in a subresource request originating from a cross-origin iframe. This forged value bypasses validation in CorsURLLoaderFactory for iframe subresources. When the target server responds with Clear-Site-Data: "cookies", the Network Service incorrectly relies on this forged site_for_cookies value to determine if the request is first-party. Consequently, the browser deletes all unpartitioned (first-party) cookies for the target site, bypassing Site Isolation guarantees that should restrict cross-site cookie clearing to partitioned state only.
(Note: These are potential steps, as our tooling agent doesn’t yet have the ability to run a live proof-of-concept.)
Potential Reproduction Steps
- Enable third-party cookie blocking in Chrome.
- A user visits
https://victim.com, which embeds a cross-origin iframe pointing tohttps://attacker.com. - The attacker compromises the renderer process hosting the
attacker.comiframe. - The compromised
attacker.comrenderer initiates afetchrequest to an endpoint on the victim’s domain (e.g.,https://victim.com/logout) that is known to return theClear-Site-Data: "cookies"HTTP response header. The attacker setscredentials_mode=kInclude. - Over the
URLLoaderFactoryMojo IPC, the attacker forges thesite_for_cookiesproperty of thenetwork::ResourceRequestto match the target site (https://victim.com). - The Network Service receives the request at
CorsURLLoaderFactory::CreateLoaderAndStart. - Inside
CorsURLLoaderFactory::IsValidRequest(services/network/cors/cors_url_loader_factory.cc), the validation ofsite_for_cookiesis skipped becauserequire_cross_site_request_for_cookies_is configured asfalsefor iframe subresource factories (content/browser/url_loader_factory_params_helper.cc). - The network request is executed with the forged
site_for_cookies. (Note: Due to SameSite protections evaluating therequest_initiator,SameSite=Lax/Strictcookies will likely not be sent. This attack relies on the endpoint unconditionally returningClear-Site-Dataor relying onSameSite=Nonecookies). - The server responds with
Clear-Site-Data: "cookies". NetworkServiceNetworkDelegate::HandleClearSiteDataHeader(services/network/network_service_network_delegate.cc) processes the header and callsCookieSettings::IsPrivacyModeEnabledto determine if only partitioned cookies should be cleared.IsPrivacyModeEnableddelegates toCookieSettingsBase::GetCookieSettingInternal, which callsIsThirdPartyRequest(request->url(), request->site_for_cookies()).- Because both values are forged to
victim.com,IsThirdPartyRequestincorrectly evaluates tofalse(first-party request). IsPrivacyModeEnabledreturnskStateAllowed, resulting inpartitioned_state_allowed_only = false.- The Network Service sends an
OnClearSiteDataIPC to the Browser process (StoragePartitionImpl), passingpartitioned_state_allowed_only = false. - The Browser process executes
SiteDataClearer, settingcookie_filter_builder->SetPartitionedCookiesOnly(false). - The
BrowsingDataRemoverdeletes all unpartitioned, first-party cookies forvictim.com, successfully executing a cross-site logout and bypassing Site Isolation protections.
Suggested Fix
The Network Service should not trust the site_for_cookies value provided by the renderer process for security-sensitive decisions like processing Clear-Site-Data.
Instead of relying on request->site_for_cookies(), NetworkServiceNetworkDelegate::HandleClearSiteDataHeader should use request->isolation_info().site_for_cookies(). The IsolationInfo is securely populated by the browser process and cannot be forged by a compromised renderer.
Alternatively, validation in CorsURLLoaderFactory::IsValidRequest could be hardened to verify that if site_for_cookies is present and matches the target URL, it must also be consistent with the securely verified request_initiator_origin_lock_ or the factory’s IsolationInfo.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.