Medium watch CVSS 3.1 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Network
DescriptionIncorrect authorization in Network
ComponentNetwork
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker497646947
Fix commitdb1fc539aaee (chromium/src) +64/-1
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-30 (per vendor advisory)
Fix landed2026-06-29 (public git)
Disclosed2026-08-25
Patch gap57 days — fix public in source before the advisory

Background

Clear-Site-Data
A response header letting a site instruct the browser to clear its cookies, storage, and/or cache; its notification to the browser carries a partitioned_state_allowed_only flag limiting clearing to partitioned state.
`IsolationInfo`
The trusted, factory-supplied description of a request’s embedding context (top-frame origin, frame origin, and SiteForCookies), provided via URLLoaderFactoryParams or trusted_params.
`site_for_cookies`
The first-party context used for SameSite/third-party-cookie decisions; a per-URLRequest value that can differ from the IsolationInfo’s copy and is comparatively less trustworthy.
`IsPrivacyModeEnabled` / `partitioned_state_allowed_only`
cookie_settings.IsPrivacyModeEnabled(url, site_for_cookies, top_frame_origin, overrides) decides the privacy setting, which drives whether only partitioned state may be cleared when third-party cookies are blocked.

Root Cause Analysis

In the Clear-Site-Data handling path (the commit identifies it as NetworkServiceNetworkDelegate::HandleClearSiteDataHeader), IsPrivacyModeEnabled was called with request->site_for_cookies() for the site-for-cookies argument but with request->isolation_info().top_frame_origin() for the top-frame argument. These two inputs could disagree: the per-request site_for_cookies might claim a first-party context while the factory’s trusted IsolationInfo describes a cross-site subframe. When third-party cookies are blocked, that mismatch could make IsPrivacyModeEnabled return a less-restrictive setting, so partitioned_state_allowed_only was computed as false and the resulting OnClearSiteData notification permitted clearing unpartitioned (global) state rather than only partitioned state.

The fix reads request->isolation_info().site_for_cookies() instead, so both the site-for-cookies and top-frame-origin inputs come from the same trusted IsolationInfo and agree on the embedding context. The added unit test builds a factory whose IsolationInfo describes a cross-site subframe with third-party cookies blocked, issues a request whose site_for_cookies falsely claims a first-party context, and asserts partitioned_state_allowed_only is reported true.

Key insight
The core mistake was mixing a less-trusted per-request site_for_cookies with a trusted IsolationInfo top-frame origin when deciding privacy mode; the fix sources both from the same IsolationInfo so a request cannot claim a first-party context that contradicts its factory’s embedding description.

Attack Path

  1. Cross-site subframe with 3P cookies blocked A request originates from a cross-site subframe whose factory IsolationInfo correctly records a top-frame origin different from the frame origin, under a profile that blocks third-party cookies.
  2. Disagreeing site_for_cookies The request carries a site_for_cookies that claims a first-party context (as in the test, SiteForCookies::FromOrigin(top_frame_origin)) even though the IsolationInfo’s SiteForCookies is empty/cross-site.
  3. Privacy decision skewed HandleClearSiteDataHeader passed the request’s own site_for_cookies to IsPrivacyModeEnabled, which combined with the trusted top-frame origin yielded a setting that did not treat the context as fully third-party, so partitioned_state_allowed_only came out false.
  4. Over-broad clearing The OnClearSiteData notification then authorized clearing of unpartitioned/global site data rather than only partitioned state for the cross-site context, exceeding the intended authorization (inferred; the diff shows the flag’s value, not a downstream data-destruction sink).

Impact Assessment

The flaw let a request from a cross-site subframe (with third-party cookies blocked) cause Clear-Site-Data to be treated as if in a first-party context, clearing more than the partitioned state it should be limited to — an incorrect-authorization issue in the Network service. The advisory classifies it as an AuthorizationError (“Incorrect authorization in Network”) and does not report exploitation in the wild. Preconditions are a cross-site embedding, third-party cookie blocking enabled, and a response bearing a Clear-Site-Data header; any concrete cross-partition state impact is (inferred).

Changed Functions

FunctionChangeNotes
TestNetworkAnnotationMonitor
services/network/network_service_unittest.cc
modified

Files Changed

  • services/network/network_service_network_delegate.cc
  • services/network/network_service_unittest.cc

Audit Directions

  • Trusted vs per-request inputs
    Look for security decisions that combine one field from the trusted IsolationInfo with another taken from the mutable per-request URLRequest (e.g. site_for_cookies, initiator), which can be made to disagree.
  • Clear-Site-Data / storage-clearing paths
    Review other consumers of IsPrivacyModeEnabled and partitioned-state flags to confirm they derive first-party context from IsolationInfo rather than request-supplied values.
  • Cross-site subframe authorization
    Audit network-delegate callbacks that gate access to unpartitioned state to ensure cross-site subframes cannot claim first-party privileges via a spoofed site_for_cookies.
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() {
Loading diff…

Regression Test / PoC

shipped with the fix
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() {
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • components/content_settings/core/common/cookie_settings_base.cc
  • services/network/cors/cors_url_loader_factory.cc
  • content/browser/storage_partition_impl.cc
  • content/browser/browsing_data/clear_site_data_utils.cc
  • net/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

  1. Enable third-party cookie blocking in Chrome.
  2. A user visits https://victim.com, which embeds a cross-origin iframe pointing to https://attacker.com.
  3. The attacker compromises the renderer process hosting the attacker.com iframe.
  4. The compromised attacker.com renderer initiates a fetch request to an endpoint on the victim’s domain (e.g., https://victim.com/logout) that is known to return the Clear-Site-Data: "cookies" HTTP response header. The attacker sets credentials_mode=kInclude.
  5. Over the URLLoaderFactory Mojo IPC, the attacker forges the site_for_cookies property of the network::ResourceRequest to match the target site (https://victim.com).
  6. The Network Service receives the request at CorsURLLoaderFactory::CreateLoaderAndStart.
  7. Inside CorsURLLoaderFactory::IsValidRequest (services/network/cors/cors_url_loader_factory.cc), the validation of site_for_cookies is skipped because require_cross_site_request_for_cookies_ is configured as false for iframe subresource factories (content/browser/url_loader_factory_params_helper.cc).
  8. The network request is executed with the forged site_for_cookies. (Note: Due to SameSite protections evaluating the request_initiator, SameSite=Lax/Strict cookies will likely not be sent. This attack relies on the endpoint unconditionally returning Clear-Site-Data or relying on SameSite=None cookies).
  9. The server responds with Clear-Site-Data: "cookies".
  10. NetworkServiceNetworkDelegate::HandleClearSiteDataHeader (services/network/network_service_network_delegate.cc) processes the header and calls CookieSettings::IsPrivacyModeEnabled to determine if only partitioned cookies should be cleared.
  11. IsPrivacyModeEnabled delegates to CookieSettingsBase::GetCookieSettingInternal, which calls IsThirdPartyRequest(request->url(), request->site_for_cookies()).
  12. Because both values are forged to victim.com, IsThirdPartyRequest incorrectly evaluates to false (first-party request).
  13. IsPrivacyModeEnabled returns kStateAllowed, resulting in partitioned_state_allowed_only = false.
  14. The Network Service sends an OnClearSiteData IPC to the Browser process (StoragePartitionImpl), passing partitioned_state_allowed_only = false.
  15. The Browser process executes SiteDataClearer, setting cookie_filter_builder->SetPartitionedCookiesOnly(false).
  16. The BrowsingDataRemover deletes all unpartitioned, first-party cookies for victim.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.

View on issue tracker