Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Workers
DescriptionUse after free in Workers
ComponentWorkers
Sub-componentSAA
Bug ClassUAF
Tracker553144392
Fix commitada1d28f4da4 (chromium/src) +211/-81
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
content/browser/worker_host/shared_worker_host.cc
modified
TEST_F
content/browser/worker_host/shared_worker_host_unittest.cc
modified
IN_PROC_BROWSER_TEST_F
content/browser/worker_host/worker_browsertest.cc
modified

Files Changed

  • content/browser/worker_host/worker_browsertest.cc
  • content/browser/worker_host/shared_worker_host.cc
  • content/browser/worker_host/shared_worker_host_unittest.cc
  • content/browser/worker_host/shared_worker_host.h
From ada1d28f4da4fe9d63dd1274c5ecb43eb5c0577e Mon Sep 17 00:00:00 2001
From: Chris Fredrickson <cfredric@chromium.org>
Date: Tue, 22 Sep 2026 15:09:12 -0700
Subject: [PATCH] [SAA] Make SharedWorker subresource fetches use x-site SiteForCookies

Subresource fetches from SharedWorkers that were created in a cross-site
context via StorageAccessHandle.SharedWorker() currently include 1P
partitioned cookies (i.e. CHIPS cookies whose AncestorChainBit is
kSameSite, meaning the cookie was set in a context where every ancestor
frame was same-site to the embedded frame).

This is not a privacy issue, since by assumption, the SharedWorker was
created via handle.SharedWorker, which means it has access to
unpartitioned storage/cookies. However, this is inconsistent with how
Chrome handles _WebSocket_ requests from these SharedWorkers: those
requests only send SameSite=None cookies and 3P CHIPS cookies.
Additionally, there is no other way for a cross-site context to cause 1P
CHIPS cookies to be sent, so depending on how sites use those cookies,
this may have security implications. We should fix this so that
subresources fetches are consistent with WebSocket requests, and only
send cross-site data.

Fixed: 553144392
Change-Id: I452769b0731c9cd9b49b67e747c56ecdd7345f3f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8444577
Auto-Submit: Chris Fredrickson <cfredric@chromium.org>
Reviewed-by: Dave Tapuska <dtapuska@chromium.org>
Commit-Queue: Chris Fredrickson <cfredric@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1702985}
---
diff --git a/content/browser/worker_host/shared_worker_host.cc b/content/browser/worker_host/shared_worker_host.cc
index aec63a1b9..a11849b1 100644
--- a/content/browser/worker_host/shared_worker_host.cc
+++ b/content/browser/worker_host/shared_worker_host.cc
@@ -535,7 +535,7 @@
       url_loader_factory::ContentClientParams(
           GetProcessHost()->GetBrowserContext(),
           /*frame=*/nullptr, GetProcessHost()->GetDeprecatedID(), origin,
-          GetWorkerStorageKey().ToPartialNetIsolationInfo(),
+          ComputeIsolationInfoForNetworkRequest(/*is_websocket_request=*/false),
           ukm::SourceIdObj::FromInt64(ukm_source_id_), bypass_redirect_checks),
       devtools_instrumentation::WillCreateURLLoaderFactoryParams::
           ForSharedWorker(this));
@@ -561,7 +561,7 @@
   network::mojom::URLLoaderFactoryParamsPtr factory_params =
       URLLoaderFactoryParamsHelper::CreateForWorker(
           GetProcessHost(), origin_lock,
-          GetWorkerStorageKey().ToPartialNetIsolationInfo(),
+          ComputeIsolationInfoForNetworkRequest(/*is_websocket_request=*/false),
           std::move(coep_reporter), std::move(dip_reporter),
           GetStoragePartitionImpl()
               ->CreateURLLoaderNetworkObserverForServiceOrSharedWorker(
@@ -719,35 +719,37 @@
           GlobalRenderFrameHostId(GetProcessHost()->GetID(),
                                   IPC::mojom::kRoutingIdNone),
           WeakDocumentPtr(), storage_key.origin(),
-          ComputeIsolationInfoForWebSocket(),
+          ComputeIsolationInfoForNetworkRequest(/*is_websocket_request=*/true),
           worker_client_security_state_->Clone(),
           net::StorageAccessApiStatus::kNone, network_restrictions_id_,
           GetDevToolsToken()),
       std::move(receiver));
 }
 
-net::IsolationInfo SharedWorkerHost::ComputeIsolationInfoForWebSocket() const {
+net::IsolationInfo SharedWorkerHost::ComputeIsolationInfoForNetworkRequest(
+    bool is_websocket_request) const {
   const blink::StorageKey& storage_key = GetWorkerStorageKey();
   net::IsolationInfo isolation_info = storage_key.ToPartialNetIsolationInfo();
 
-  base::UmaHistogramBoolean(
-      "Content.SharedWorker.WebSocket.DoesRequireCrossSiteRequestForCookies",
-      instance_.DoesRequireCrossSiteRequestForCookies());
+  if (is_websocket_request) {
+    base::UmaHistogramBoolean(
+        "Content.SharedWorker.WebSocket.DoesRequireCrossSiteRequestForCookies",
+        instance_.DoesRequireCrossSiteRequestForCookies());
+  }
 
-  if (instance_.DoesRequireCrossSiteRequestForCookies()) {
-    if (base::FeatureList::IsEnabled(
-            features::kRestrictSharedWorkerWebSocketCrossSiteCookies)) {
-      // If the worker requires cross-site cookie semantics (e.g. a worker in a
-      // third-party context or created via the Storage Access API), we must
-      // ensure that the SiteForCookies is null. This prevents the network
-      // service from incorrectly attaching SameSite=Strict/Lax cookies to the
-      // WebSocket handshake.
-      CHECK(!isolation_info.IsEmpty());
-      isolation_info = net::IsolationInfo::Create(
-          isolation_info.request_type(), *isolation_info.top_frame_origin(),
-          *isolation_info.frame_origin(), net::SiteForCookies(),
-          isolation_info.nonce());
-    }
+  if (instance_.DoesRequireCrossSiteRequestForCookies() &&
+      base::FeatureList::IsEnabled(
+          features::kRestrictSharedWorkerWebSocketCrossSiteCookies)) {
+    // If the worker requires cross-site cookie semantics (e.g. a worker in a
+    // third-party context or created via the Storage Access API), we must
+    // ensure that the SiteForCookies is null. This prevents the network service
+    // from incorrectly attaching SameSite=Strict/Lax or first-party CHIPS
+    // cookies to the request.
+    CHECK(!isolation_info.IsEmpty());
+    isolation_info = net::IsolationInfo::Create(
+        isolation_info.request_type(), *isolation_info.top_frame_origin(),
+        *isolation_info.frame_origin(), net::SiteForCookies(),
+        isolation_info.nonce());
   }
   return isolation_info;
 }
diff --git a/content/browser/worker_host/shared_worker_host.h b/content/browser/worker_host/shared_worker_host.h
index 3e8ef4a..7ddb670f 100644
--- a/content/browser/worker_host/shared_worker_host.h
+++ b/content/browser/worker_host/shared_worker_host.h
@@ -236,8 +236,12 @@
     worker_client_security_state_ = std::move(client_security_state);
   }
 
-  // Computes the IsolationInfo used for WebSocket connections from this worker.
-  net::IsolationInfo ComputeIsolationInfoForWebSocket() const;
+  // Computes the IsolationInfo used for network requests from this worker. The
+  // SiteForCookies is nulled out when the worker requires cross-site request
+  // semantics for cookies (e.g. a worker in a third-party context, or one
+  // created with a first-party storage key via the Storage Access API).
+  net::IsolationInfo ComputeIsolationInfoForNetworkRequest(
+      bool is_websocket_request) const;
 
   base::WeakPtr<SharedWorkerHost> AsWeakPtr();
 
diff --git a/content/browser/worker_host/shared_worker_host_unittest.cc b/content/browser/worker_host/shared_worker_host_unittest.cc
index 193b9ba..daab9f1 100644
--- a/content/browser/worker_host/shared_worker_host_unittest.cc
+++ b/content/browser/worker_host/shared_worker_host_unittest.cc
@@ -36,6 +36,9 @@
 #include "content/public/test/test_utils.h"
 #include "mojo/public/cpp/bindings/self_owned_receiver.h"
 #include "mojo/public/cpp/test_support/test_utils.h"
+#include "net/base/schemeful_site.h"
+#include "net/cookies/cookie_partition_key.h"
+#include "net/cookies/site_for_cookies.h"
 #include "services/network/public/cpp/cross_origin_embedder_policy.h"
 #include "services/network/public/cpp/features.h"
 #include "services/network/public/cpp/not_implemented_url_loader_factory.h"
@@ -419,6 +422,49 @@
   EXPECT_EQ(host->GetWorkerStorageKey().origin(),
             params->isolation_info.frame_origin());
   EXPECT_FALSE(params->isolation_info.nonce().has_value());
+  EXPECT_TRUE(
+      params->isolation_info.site_for_cookies().IsFirstParty(kWorkerUrl));
+  EXPECT_EQ(net::CookiePartitionKey::FromNetworkIsolationKey(
+                params->isolation_info.network_isolation_key(),
+                params->isolation_info.site_for_cookies(),
+                net::SchemefulSite(kWorkerUrl),
+                /*main_frame_navigation=*/false),
+            net::CookiePartitionKey::FromWire(
+                net::SchemefulSite(kWorkerUrl),
+                net::CookiePartitionKey::AncestorChainBit::kSameSite));
+}
+
+TEST_F(SharedWorkerHostTest,
+       CreateNetworkFactoryParamsForSubresources_CrossSiteCookieRestrictions) {
+  // Create a SharedWorkerHost with a first-party storage key and
+  // SharedWorkerSameSiteCookies::kNone (e.g. created in a third-party context
+  // via the Storage Access API or with `sameSiteCookies: 'none'`).
+  base::WeakPtr<SharedWorkerHost> host = CreateHostWithSameSiteCookies(
+      blink::mojom::SharedWorkerSameSiteCookies::kNone);
+
+  // Start the worker.
+  mojo::PendingRemote<blink::mojom::SharedWorkerFactory> factory;
+  MockSharedWorkerFactory factory_impl(
+      factory.InitWithNewPipeAndPassReceiver());
+  StartWorker(host.get(), std::move(factory));
+
+  network::mojom::URLLoaderFactoryParamsPtr params =
+      host->CreateNetworkFactoryParamsForSubresources();
+  EXPECT_EQ(host->GetWorkerStorageKey().origin(),
+            params->isolation_info.top_frame_origin());
+  EXPECT_EQ(host->GetWorkerStorageKey().origin(),
+            params->isolation_info.frame_origin());
+  // SiteForCookies must be null so that subresource requests do not derive a
+  // first-party (kSameSite) CHIPS cookie partition key.
+  EXPECT_TRUE(params->isolation_info.site_for_cookies().IsNull());
+  EXPECT_EQ(net::CookiePartitionKey::FromNetworkIsolationKey(
+                params->isolation_info.network_isolation_key(),
+                params->isolation_info.site_for_cookies(),
+                net::SchemefulSite(kWorkerUrl),
+                /*main_frame_navigation=*/false),
+            net::CookiePartitionKey::FromWire(
+                net::SchemefulSite(kWorkerUrl),
+                net::CookiePartitionKey::AncestorChainBit::kCrossSite));
 }
 
 TEST_F(SharedWorkerHostTest, CreateBlobUrlStoreProvider) {
@@ -640,7 +686,9 @@
       network::mojom::ClientSecurityState::New());
 
   base::HistogramTester histogram_tester;
-  net::IsolationInfo isolation_info = host->ComputeIsolationInfoForWebSocket();
+  net::IsolationInfo isolation_info =
+      host->ComputeIsolationInfoForNetworkRequest(
+          /*is_websocket_request=*/true);
   // SiteForCookies should NOT be null for same-origin workers.
   EXPECT_FALSE(isolation_info.site_for_cookies().IsNull());
 
@@ -686,7 +734,8 @@
     base::HistogramTester histogram_tester;
 
     net::IsolationInfo isolation_info =
-        host->ComputeIsolationInfoForWebSocket();
+        host->ComputeIsolationInfoForNetworkRequest(
+            /*is_websocket_request=*/true);
     EXPECT_FALSE(isolation_info.site_for_cookies().IsNull());
 
     histogram_tester.ExpectUniqueSample(
@@ -703,7 +752,8 @@
     base::HistogramTester histogram_tester;
 
     net::IsolationInfo isolation_info =
-        host->ComputeIsolationInfoForWebSocket();
+        host->ComputeIsolationInfoForNetworkRequest(
+            /*is_websocket_request=*/true);
     EXPECT_TRUE(isolation_info.site_for_cookies().IsNull());
 
     histogram_tester.ExpectUniqueSample(
diff --git a/content/browser/worker_host/worker_browsertest.cc b/content/browser/worker_host/worker_browsertest.cc
index 80906dc6..39c16cc 100644
--- a/content/browser/worker_host/worker_browsertest.cc
+++ b/content/browser/worker_host/worker_browsertest.cc
@@ -17,11 +17,13 @@
 #include "base/system/sys_info.h"
 #include "base/test/bind.h"
 #include "base/test/scoped_feature_list.h"
+#include "base/test/test_future.h"
 #include "base/test/test_timeouts.h"
 #include "base/thread_annotations.h"
 #include "base/threading/thread_restrictions.h"
 #include "build/build_config.h"
 #include "content/browser/bad_message.h"
+#include "content/browser/permissions/permission_controller_impl.h"
 #include "content/browser/process_lock.h"
 #include "content/browser/renderer_host/render_frame_host_impl.h"
 #include "content/browser/security/cpsp/child_process_security_policy_impl.h"
@@ -51,8 +53,10 @@
 #include "content/test/content_browser_test_utils_internal.h"
 #include "net/base/features.h"
 #include "net/base/filename_util.h"
+#include "net/base/schemeful_site.h"
 #include "net/cookies/canonical_cookie.h"
 #include "net/cookies/cookie_access_result.h"
+#include "net/cookies/cookie_partition_key.h"
 #include "net/dns/mock_host_resolver.h"
 #include "net/http/http_response_headers.h"
 #include "net/ssl/client_cert_identity.h"
@@ -74,6 +78,7 @@
 #include "services/network/public/mojom/parsed_headers.mojom.h"
 #include "services/network/public/mojom/url_response_head.mojom.h"
 #include "third_party/blink/public/common/features.h"
+#include "third_party/blink/public/common/permissions/permission_utils.h"
 #include "third_party/blink/public/common/storage_key/storage_key.h"
 #include "url/gurl.h"
 
@@ -81,7 +86,16 @@
 
 namespace {
 
-const char kSameSiteCookie[] = "same-site-cookie=same-site-cookie-value";
+const char kSameSiteLaxCookie[] =
+    "same-site-lax-cookie=same-site-lax-cookie-value";
+const char kSameSiteStrictCookie[] =
+    "same-site-strict-cookie=same-site-strict-cookie-value";
+const char kSameSiteNoneCookie[] =
+    "same-site-none-cookie=same-site-none-cookie-value";
+const char kFirstPartyPartitionedCookie[] =
+    "first-party-partitioned=partitioned-cookie-value";
+const char kCrossSitePartitionedCookie[] =
+    "cross-site-partitioned=partitioned-cookie-value";
 
 // Used by both the embedded test server when a header specified by
 // "/echoheader" is missing, and by the test fixture when there's no cookie
@@ -245,33 +259,26 @@
     runner->Run();
   }
 
-  void SetSameSiteCookie(const std::string& host) {
-    StoragePartition* partition = shell()
-                                      ->web_contents()
-                                      ->GetBrowserContext()
-                                      ->GetDefaultStoragePartition();
-    mojo::Remote<network::mojom::CookieManager> cookie_manager;
-    partition->GetNetworkContext()->GetCookieManager(
-        cookie_manager.BindNewPipeAndPassReceiver());
-    net::CookieOptions options;
-    options.set_same_site_cookie_context(
-        net::CookieOptions::SameSiteCookieContext(
-            net::CookieOptions::SameSiteCookieContext::ContextType::
-                SAME_SITE_LAX));
+  void SetSameSiteLaxCookie(std::string_view host) {
+    ASSERT_TRUE(SetCookie(
+        shell()->web_contents()->GetBrowserContext(),
+        ssl_server_.GetURL(host, "/"),
+        base::StrCat({kSameSiteLaxCookie, "; SameSite=Lax; Secure"})));
+  }
+
+  void SetPartitionedCookie(
+      std::string_view host,
+      std::string_view cookie_name_value,
+      net::CookiePartitionKey::AncestorChainBit ancestor_chain_bit) {
     GURL cookie_url = ssl_server_.GetURL(host, "/");
-    std::unique_ptr<net::CanonicalCookie> cookie =
-        net::CanonicalCookie::CreateForTesting(
-            cookie_url, std::string(kSameSiteCookie) + "; SameSite=Lax; Secure",
-            base::Time::Now(), net::CookieSourceType::kOther);
-    base::RunLoop run_loop;
-    cookie_manager->SetCanonicalCookie(
-        *cookie, cookie_url, options,
-        base::BindLambdaForTesting(
-            [&](net::CookieAccessResult set_cookie_result) {
-              EXPECT_TRUE(set_cookie_result.status.IsInclude());
-              run_loop.Quit();
-            }));
-    run_loop.Run();
+    net::CookiePartitionKey partition_key = net::CookiePartitionKey::FromWire(
+        net::SchemefulSite(cookie_url), ancestor_chain_bit);
+    ASSERT_TRUE(
+        SetCookie(shell()->web_contents()->GetBrowserContext(), cookie_url,
+                  base::StrCat({cookie_name_value,
+                                "; SameSite=None; Secure; Partitioned"}),
+                  net::CookieOptions::SameSiteCookieContext::MakeInclusive(),
+                  partition_key));
   }
 
   // Returns the cookie received with the request for the specified path. If the
@@ -981,8 +988,8 @@
   const GURL resource_url(
       ssl_server()->GetURL(cross_site_domain, "/workers/empty.html"));
 
-  // Set a cookie for verfifying which requests send SameSite cookies.
-  SetSameSiteCookie(cross_site_domain);
+  // Set a cookie for verifying which requests send SameSite cookies.
+  SetSameSiteLaxCookie(cross_site_domain);
 
   std::set<GURL> expected_request_urls = {worker_url, script_url, resource_url};
   const url::Origin expected_origin =
@@ -1020,28 +1027,28 @@
 // Test that an "a.test" worker sends "a.test" SameSite cookies, both when
 // requesting the worker script and when fetching other resources.
 IN_PROC_BROWSER_TEST_F(WorkerTest, WorkerSameSiteCookies1) {
-  SetSameSiteCookie("a.test");
+  SetSameSiteLaxCookie("a.test");
   ASSERT_TRUE(NavigateToURL(
       shell(),
       ssl_server()->GetURL(
           "a.test",
           "/workers/create_worker.html?worker_url=fetch_from_worker.js")));
-  EXPECT_EQ(kSameSiteCookie,
+  EXPECT_EQ(kSameSiteLaxCookie,
             EvalJs(shell()->web_contents(),
                    "worker.postMessage({url: '/echoheader?Cookie'}); "
                    "waitForMessage();"));
-  EXPECT_EQ(kSameSiteCookie,
+  EXPECT_EQ(kSameSiteLaxCookie,
             GetReceivedCookie(
                 "/workers/create_worker.html?worker_url=fetch_from_worker.js"));
-  EXPECT_EQ(kSameSiteCookie,
+  EXPECT_EQ(kSameSiteLaxCookie,
             GetReceivedCookie("/workers/fetch_from_worker.js"));
-  EXPECT_EQ(kSameSiteCookie, GetReceivedCookie("/echoheader?Cookie"));
+  EXPECT_EQ(kSameSiteLaxCookie, GetReceivedCookie("/echoheader?Cookie"));
 }
 
 // Test that a "b.test" worker does not send "a.test" SameSite cookies when
 // fetching resources.
 IN_PROC_BROWSER_TEST_F(WorkerTest, WorkerSameSiteCookies2) {
-  SetSameSiteCookie("a.test");
+  SetSameSiteLaxCookie("a.test");
   ASSERT_TRUE(NavigateToURL(
       shell(),
       ssl_server()->GetURL(
@@ -1060,26 +1067,26 @@
 // Test that an "a.test" nested worker sends "a.test" SameSite cookies, both
 // when requesting the worker script and when fetching other resources.
 IN_PROC_BROWSER_TEST_F(WorkerTest, NestedWorkerSameSiteCookies) {
-  SetSameSiteCookie("a.test");
+  SetSameSiteLaxCookie("a.test");
   ASSERT_TRUE(NavigateToURL(
       shell(),
       ssl_server()->GetURL(
           "a.test",
           "/workers/"
           "create_worker.html?worker_url=fetch_from_nested_worker.js")));
-  EXPECT_EQ(kSameSiteCookie,
+  EXPECT_EQ(kSameSiteLaxCookie,
             EvalJs(shell()->web_contents(),
                    "worker.postMessage({url: '/echoheader?Cookie'}); "
                    "waitForMessage();"));
-  EXPECT_EQ(kSameSiteCookie,
+  EXPECT_EQ(kSameSiteLaxCookie,
             GetReceivedCookie(
                 "/workers/"
                 "create_worker.html?worker_url=fetch_from_nested_worker.js"));
-  EXPECT_EQ(kSameSiteCookie,
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/worker_host/shared_worker_host_unittest.cc b/content/browser/worker_host/shared_worker_host_unittest.cc
index 193b9ba..daab9f1 100644
--- a/content/browser/worker_host/shared_worker_host_unittest.cc
+++ b/content/browser/worker_host/shared_worker_host_unittest.cc
@@ -36,6 +36,9 @@
 #include "content/public/test/test_utils.h"
 #include "mojo/public/cpp/bindings/self_owned_receiver.h"
 #include "mojo/public/cpp/test_support/test_utils.h"
+#include "net/base/schemeful_site.h"
+#include "net/cookies/cookie_partition_key.h"
+#include "net/cookies/site_for_cookies.h"
 #include "services/network/public/cpp/cross_origin_embedder_policy.h"
 #include "services/network/public/cpp/features.h"
 #include "services/network/public/cpp/not_implemented_url_loader_factory.h"
@@ -419,6 +422,49 @@
   EXPECT_EQ(host->GetWorkerStorageKey().origin(),
             params->isolation_info.frame_origin());
   EXPECT_FALSE(params->isolation_info.nonce().has_value());
+  EXPECT_TRUE(
+      params->isolation_info.site_for_cookies().IsFirstParty(kWorkerUrl));
+  EXPECT_EQ(net::CookiePartitionKey::FromNetworkIsolationKey(
+                params->isolation_info.network_isolation_key(),
+                params->isolation_info.site_for_cookies(),
+                net::SchemefulSite(kWorkerUrl),
+                /*main_frame_navigation=*/false),
+            net::CookiePartitionKey::FromWire(
+                net::SchemefulSite(kWorkerUrl),
+                net::CookiePartitionKey::AncestorChainBit::kSameSite));
+}
+
+TEST_F(SharedWorkerHostTest,
+       CreateNetworkFactoryParamsForSubresources_CrossSiteCookieRestrictions) {
+  // Create a SharedWorkerHost with a first-party storage key and
+  // SharedWorkerSameSiteCookies::kNone (e.g. created in a third-party context
+  // via the Storage Access API or with `sameSiteCookies: 'none'`).
+  base::WeakPtr<SharedWorkerHost> host = CreateHostWithSameSiteCookies(
+      blink::mojom::SharedWorkerSameSiteCookies::kNone);
+
+  // Start the worker.
+  mojo::PendingRemote<blink::mojom::SharedWorkerFactory> factory;
+  MockSharedWorkerFactory factory_impl(
+      factory.InitWithNewPipeAndPassReceiver());
+  StartWorker(host.get(), std::move(factory));
+
+  network::mojom::URLLoaderFactoryParamsPtr params =
+      host->CreateNetworkFactoryParamsForSubresources();
+  EXPECT_EQ(host->GetWorkerStorageKey().origin(),
+            params->isolation_info.top_frame_origin());
+  EXPECT_EQ(host->GetWorkerStorageKey().origin(),
+            params->isolation_info.frame_origin());
+  // SiteForCookies must be null so that subresource requests do not derive a
+  // first-party (kSameSite) CHIPS cookie partition key.
+  EXPECT_TRUE(params->isolation_info.site_for_cookies().IsNull());
+  EXPECT_EQ(net::CookiePartitionKey::FromNetworkIsolationKey(
+                params->isolation_info.network_isolation_key(),
+                params->isolation_info.site_for_cookies(),
+                net::SchemefulSite(kWorkerUrl),
+                /*main_frame_navigation=*/false),
+            net::CookiePartitionKey::FromWire(
+                net::SchemefulSite(kWorkerUrl),
+                net::CookiePartitionKey::AncestorChainBit::kCrossSite));
 }
 
 TEST_F(SharedWorkerHostTest, CreateBlobUrlStoreProvider) {
@@ -640,7 +686,9 @@
       network::mojom::ClientSecurityState::New());
 
   base::HistogramTester histogram_tester;
-  net::IsolationInfo isolation_info = host->ComputeIsolationInfoForWebSocket();
+  net::IsolationInfo isolation_info =
+      host->ComputeIsolationInfoForNetworkRequest(
+          /*is_websocket_request=*/true);
   // SiteForCookies should NOT be null for same-origin workers.
   EXPECT_FALSE(isolation_info.site_for_cookies().IsNull());
 
@@ -686,7 +734,8 @@
     base::HistogramTester histogram_tester;
 
     net::IsolationInfo isolation_info =
-        host->ComputeIsolationInfoForWebSocket();
+        host->ComputeIsolationInfoForNetworkRequest(
+            /*is_websocket_request=*/true);
     EXPECT_FALSE(isolation_info.site_for_cookies().IsNull());
 
     histogram_tester.ExpectUniqueSample(
@@ -703,7 +752,8 @@
     base::HistogramTester histogram_tester;
 
     net::IsolationInfo isolation_info =
-        host->ComputeIsolationInfoForWebSocket();
+        host->ComputeIsolationInfoForNetworkRequest(
+            /*is_websocket_request=*/true);
     EXPECT_TRUE(isolation_info.site_for_cookies().IsNull());
 
     histogram_tester.ExpectUniqueSample(
diff --git a/content/browser/worker_host/worker_browsertest.cc b/content/browser/worker_host/worker_browsertest.cc
index 80906dc6..39c16cc 100644
--- a/content/browser/worker_host/worker_browsertest.cc
+++ b/content/browser/worker_host/worker_browsertest.cc
@@ -17,11 +17,13 @@
 #include "base/system/sys_info.h"
 #include "base/test/bind.h"
 #include "base/test/scoped_feature_list.h"
+#include "base/test/test_future.h"
 #include "base/test/test_timeouts.h"
 #include "base/thread_annotations.h"
 #include "base/threading/thread_restrictions.h"
 #include "build/build_config.h"
 #include "content/browser/bad_message.h"
+#include "content/browser/permissions/permission_controller_impl.h"
 #include "content/browser/process_lock.h"
 #include "content/browser/renderer_host/render_frame_host_impl.h"
 #include "content/browser/security/cpsp/child_process_security_policy_impl.h"
@@ -51,8 +53,10 @@
 #include "content/test/content_browser_test_utils_internal.h"
 #include "net/base/features.h"
 #include "net/base/filename_util.h"
+#include "net/base/schemeful_site.h"
 #include "net/cookies/canonical_cookie.h"
 #include "net/cookies/cookie_access_result.h"
+#include "net/cookies/cookie_partition_key.h"
 #include "net/dns/mock_host_resolver.h"
 #include "net/http/http_response_headers.h"
 #include "net/ssl/client_cert_identity.h"
@@ -74,6 +78,7 @@
 #include "services/network/public/mojom/parsed_headers.mojom.h"
 #include "services/network/public/mojom/url_response_head.mojom.h"
 #include "third_party/blink/public/common/features.h"
+#include "third_party/blink/public/common/permissions/permission_utils.h"
 #include "third_party/blink/public/common/storage_key/storage_key.h"
 #include "url/gurl.h"
 
@@ -81,7 +86,16 @@
 
 namespace {
 
-const char kSameSiteCookie[] = "same-site-cookie=same-site-cookie-value";
+const char kSameSiteLaxCookie[] =
+    "same-site-lax-cookie=same-site-lax-cookie-value";
+const char kSameSiteStrictCookie[] =
+    "same-site-strict-cookie=same-site-strict-cookie-value";
+const char kSameSiteNoneCookie[] =
+    "same-site-none-cookie=same-site-none-cookie-value";
+const char kFirstPartyPartitionedCookie[] =
+    "first-party-partitioned=partitioned-cookie-value";
+const char kCrossSitePartitionedCookie[] =
+    "cross-site-partitioned=partitioned-cookie-value";
 
 // Used by both the embedded test server when a header specified by
 // "/echoheader" is missing, and by the test fixture when there's no cookie
@@ -245,33 +259,26 @@
     runner->Run();
   }
 
-  void SetSameSiteCookie(const std::string& host) {
-    StoragePartition* partition = shell()
-                                      ->web_contents()
-                                      ->GetBrowserContext()
-                                      ->GetDefaultStoragePartition();
-    mojo::Remote<network::mojom::CookieManager> cookie_manager;
-    partition->GetNetworkContext()->GetCookieManager(
-        cookie_manager.BindNewPipeAndPassReceiver());
-    net::CookieOptions options;
-    options.set_same_site_cookie_context(
-        net::CookieOptions::SameSiteCookieContext(
-            net::CookieOptions::SameSiteCookieContext::ContextType::
-                SAME_SITE_LAX));
+  void SetSameSiteLaxCookie(std::string_view host) {
+    ASSERT_TRUE(SetCookie(
+        shell()->web_contents()->GetBrowserContext(),
+        ssl_server_.GetURL(host, "/"),
+        base::StrCat({kSameSiteLaxCookie, "; SameSite=Lax; Secure"})));
+  }
+
+  void SetPartitionedCookie(
+      std::string_view host,
+      std::string_view cookie_name_value,
+      net::CookiePartitionKey::AncestorChainBit ancestor_chain_bit) {
     GURL cookie_url = ssl_server_.GetURL(host, "/");
-    std::unique_ptr<net::CanonicalCookie> cookie =
-        net::CanonicalCookie::CreateForTesting(
-            cookie_url, std::string(kSameSiteCookie) + "; SameSite=Lax; Secure",
-            base::Time::Now(), net::CookieSourceType::kOther);
-    base::RunLoop run_loop;
-    cookie_manager->SetCanonicalCookie(
-        *cookie, cookie_url, options,
-        base::BindLambdaForTesting(
-            [&](net::CookieAccessResult set_cookie_result) {
-              EXPECT_TRUE(set_cookie_result.status.IsInclude());
-              run_loop.Quit();
-            }));
-    run_loop.Run();
+    net::CookiePartitionKey partition_key = net::CookiePartitionKey::FromWire(
+        net::SchemefulSite(cookie_url), ancestor_chain_bit);
+    ASSERT_TRUE(
+        SetCookie(shell()->web_contents()->GetBrowserContext(), cookie_url,
+                  base::StrCat({cookie_name_value,
+                                "; SameSite=None; Secure; Partitioned"}),
+                  net::CookieOptions::SameSiteCookieContext::MakeInclusive(),
+                  partition_key));
   }
 
   // Returns the cookie received with the request for the specified path. If the
@@ -981,8 +988,8 @@
   const GURL resource_url(
       ssl_server()->GetURL(cross_site_domain, "/workers/empty.html"));
 
-  // Set a cookie for verfifying which requests send SameSite cookies.
-  SetSameSiteCookie(cross_site_domain);
+  // Set a cookie for verifying which requests send SameSite cookies.
+  SetSameSiteLaxCookie(cross_site_domain);
 
   std::set<GURL> expected_request_urls = {worker_url, script_url, resource_url};
   const url::Origin expected_origin =
@@ -1020,28 +1027,28 @@
 // Test that an "a.test" worker sends "a.test" SameSite cookies, both when
 // requesting the worker script and when fetching other resources.
 IN_PROC_BROWSER_TEST_F(WorkerTest, WorkerSameSiteCookies1) {
-  SetSameSiteCookie("a.test");
+  SetSameSiteLaxCookie("a.test");
   ASSERT_TRUE(NavigateToURL(
       shell(),
       ssl_server()->GetURL(
           "a.test",
           "/workers/create_worker.html?worker_url=fetch_from_worker.js")));
-  EXPECT_EQ(kSameSiteCookie,
+  EXPECT_EQ(kSameSiteLaxCookie,
             EvalJs(shell()->web_contents(),
                    "worker.postMessage({url: '/echoheader?Cookie'}); "
                    "waitForMessage();"));
-  EXPECT_EQ(kSameSiteCookie,
+  EXPECT_EQ(kSameSiteLaxCookie,
             GetReceivedCookie(
                 "/workers/create_worker.html?worker_url=fetch_from_worker.js"));
-  EXPECT_EQ(kSameSiteCookie,
+  EXPECT_EQ(kSameSiteLaxCookie,
             GetReceivedCookie("/workers/fetch_from_worker.js"));
-  EXPECT_EQ(kSameSiteCookie, GetReceivedCookie("/echoheader?Cookie"));
+  EXPECT_EQ(kSameSiteLaxCookie, GetReceivedCookie("/echoheader?Cookie"));
 }
 
 // Test that a "b.test" worker does not send "a.test" SameSite cookies when
 // fetching resources.
 IN_PROC_BROWSER_TEST_F(WorkerTest, WorkerSameSiteCookies2) {
-  SetSameSiteCookie("a.test");
+  SetSameSiteLaxCookie("a.test");
   ASSERT_TRUE(NavigateToURL(
       shell(),
       ssl_server()->GetURL(
@@ -1060,26 +1067,26 @@
 // Test that an "a.test" nested worker sends "a.test" SameSite cookies, both
 // when requesting the worker script and when fetching other resources.
 IN_PROC_BROWSER_TEST_F(WorkerTest, NestedWorkerSameSiteCookies) {
-  SetSameSiteCookie("a.test");
+  SetSameSiteLaxCookie("a.test");
   ASSERT_TRUE(NavigateToURL(
       shell(),
       ssl_server()->GetURL(
           "a.test",
           "/workers/"
           "create_worker.html?worker_url=fetch_from_nested_worker.js")));
-  EXPECT_EQ(kSameSiteCookie,
+  EXPECT_EQ(kSameSiteLaxCookie,
             EvalJs(shell()->web_contents(),
                    "worker.postMessage({url: '/echoheader?Cookie'}); "
                    "waitForMessage();"));
-  EXPECT_EQ(kSameSiteCookie,
+  EXPECT_EQ(kSameSiteLaxCookie,
             GetReceivedCookie(
                 "/workers/"
                 "create_worker.html?worker_url=fetch_from_nested_worker.js"));
-  EXPECT_EQ(kSameSiteCookie,
+  EXPECT_EQ(kSameSiteLaxCookie,
             GetReceivedCookie("/workers/fetch_from_nested_worker.js"));
-  EXPECT_EQ(kSameSiteCookie,
+  EXPECT_EQ(kSameSiteLaxCookie,
             GetReceivedCookie("/workers/fetch_from_worker.js"));
-  EXPECT_EQ(kSameSiteCookie, GetReceivedCookie("/echoheader?Cookie"));
+  EXPECT_EQ(kSameSiteLaxCookie, GetReceivedCookie("/echoheader?Cookie"));
 }
 
 // Test that an "a.test" iframe in a "b.test" frame does not send same-site
@@ -1087,7 +1094,7 @@
 // "a.test" resources.
 IN_PROC_BROWSER_TEST_F(WorkerTest,
                        CrossOriginIframeWorkerDoesNotSendSameSiteCookies1) {
-  SetSameSiteCookie("a.test");
+  SetSameSiteLaxCookie("a.test");
 
   ASSERT_TRUE(NavigateToURL(
       shell(), ssl_server()->GetURL("b.test", "/workers/frame_factory.html")));
@@ -1129,7 +1136,7 @@
 // cookies when its "b.test" worker requests "a.test" resources.
 IN_PROC_BROWSER_TEST_F(WorkerTest,
                        CrossOriginIframeWorkerDoesNotSendSameSiteCookies2) {
-  SetSameSiteCookie("a.test");
+  SetSameSiteLaxCookie("a.test");
 
   ASSERT_TRUE(NavigateToURL(
       shell(), ssl_server()->GetURL("a.test", "/workers/frame_factory.html")));
... (truncated)
Loading diff…

Original Bug Report

reported by vm...@google.com

First-party CHIPS cookie partition leak in SAA SharedWorker subresource factory

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: When a cross-site iframe creates a SharedWorker via the Storage Access API, the worker receives a first-party StorageKey while requiring cross-site cookie restrictions. However, SharedWorkerHost::CreateNetworkFactoryParamsForSubresources constructs the subresource URLLoaderFactory IsolationInfo without clearing the first-party SiteForCookies. As a result, subresource requests derive a CookiePartitionKey with AncestorChainBit::kSameSite, potentially allowing third-party embedded contexts to read and write first-party partitioned (CHIPS) cookies.

Affected files:

  • content/browser/worker_host/shared_worker_host.cc

Estimated timestamp from git blame: 2024-02-14

Summary

When a cross-site iframe creates a SharedWorker through the Storage Access API (document.requestStorageAccess({sharedWorker: true})), the browser grants the worker a first-party StorageKey (blink::StorageKey::CreateFirstParty(...)) to share storage with the origin. Because the creator frame is embedded cross-site, the worker is marked with DoesRequireCrossSiteRequestForCookies() == true.

However, in SharedWorkerHost::CreateNetworkFactoryParamsForSubresources(), the subresource URLLoaderFactoryParams’ IsolationInfo is initialized directly from GetWorkerStorageKey().ToPartialNetIsolationInfo(), which contains a non-null first-party SiteForCookies. When subresource requests are dispatched in the Network Service, URLRequest::set_isolation_info() derives cookie_partition_key_ using this factory IsolationInfo. Because the factory SiteForCookies is non-null and matches the request site, CookiePartitionKey::FromNetworkIsolationKey() assigns AncestorChainBit::kSameSite instead of AncestorChainBit::kCrossSite.

This derives a partition key identical to the target origin’s first-party CHIPS partition (origin, AncestorChainBit::kSameSite), allowing a third-party embedded context to potentially read and overwrite (fixate) first-party SameSite=None; Partitioned cookies.


Root Cause Analysis

  1. StorageKey Override in SAA: In content/browser/storage_access/storage_access_handle.cc:213-220, creating a SharedWorker overrides the storage key to a first-party key:

    void StorageAccessHandle::BindSharedWorker(
        mojo::PendingReceiver<blink::mojom::SharedWorkerConnector> receiver) {
      SharedWorkerConnectorImpl::Create(
          PassKey(), render_frame_host().GetGlobalId(),
          blink::StorageKey::CreateFirstParty(
              render_frame_host().GetStorageKey().origin()),
          std::move(receiver));
    }
    
  2. Cross-Site Cookie Enforcement: In content/browser/worker_host/shared_worker_service_impl.cc:162-168 and third_party/blink/renderer/core/workers/shared_worker.cc:180-183, third-party frames force same_site_cookies = mojom::SharedWorkerSameSiteCookies::kNone. Consequently, SharedWorkerInstance::DoesRequireCrossSiteRequestForCookies() (content/public/browser/shared_worker_instance.h:83-87) returns true.

  3. Factory IsolationInfo Retains First-Party SiteForCookies: In content/browser/worker_host/shared_worker_host.cc:544-558:

    network::mojom::URLLoaderFactoryParamsPtr factory_params =
        URLLoaderFactoryParamsHelper::CreateForWorker(
            GetProcessHost(), origin_lock,
            GetWorkerStorageKey().ToPartialNetIsolationInfo(),   // <-- Non-null 1P SiteForCookies
            std::move(coep_reporter), std::move(dip_reporter),
            ...
            instance_.DoesRequireCrossSiteRequestForCookies(),   // Sets factory flag only
            /*is_for_service_worker=*/false);
    

    For a first-party StorageKey, ToPartialNetIsolationInfo() embeds net::SiteForCookies(top_level_site_) (third_party/blink/common/storage_key/storage_key.cc:781-800). The require_cross_site_request_for_cookies flag is only checked in CorsURLLoaderFactory::IsValidRequest against request.site_for_cookies (which Blink nulls at fetch_manager.cc:1204-1208), but does not sanitize factory_params->isolation_info.

  4. CookiePartitionKey Derivation in Network Service: In services/network/url_loader_util.cc:497-503, url_request.set_isolation_info() receives the factory’s IsolationInfo. In net/url_request/url_request.cc:531-544 and net/cookies/cookie_partition_key.cc:174-177:

    ancestor_chain_bit = BoolToAncestorChainBit(
        !site_for_cookies.IsFirstParty(request_site.GetURL()));
    

    Because site_for_cookies is SiteForCookies(origin), IsFirstParty() evaluates to true, setting ancestor_chain_bit to kSameSite. The resulting partition key is (origin, AncestorChainBit::kSameSite).

  5. Contrast with WebSocket Implementation: The WebSocket path in SharedWorkerHost::ComputeIsolationInfoForWebSocket (content/browser/worker_host/shared_worker_host.cc:719-733) correctly nulls SiteForCookies when instance_.DoesRequireCrossSiteRequestForCookies() is true, but this was omitted for the subresource loader factory.


Potential Trigger Steps

(Note: These are suggested/potential steps derived from static code analysis, as our current tooling environment cannot execute live browser code.)

  1. Setup: Victim visits https://tracker.example directly. The server sets Set-Cookie: sid=SECRET; Secure; SameSite=None; Partitioned; Path=/, which is stored under the partition (https://tracker.example, AncestorChainBit::kSameSite).
  2. Embedding: Victim visits https://news.example, which embeds <iframe src="https://tracker.example/embed.html">.
  3. SAA Request: The iframe requests storage access: const handle = await document.requestStorageAccess({sharedWorker: true});.
  4. Worker Launch: The iframe launches a SharedWorker via const worker = handle.SharedWorker('/worker.js');.
  5. Subresource Fetch (Read): Inside /worker.js, the script issues fetch('https://tracker.example/api/session', {credentials: 'include'}). The subresource request uses CookiePartitionKey(tracker.example, kSameSite), attaching the victim’s first-party partitioned cookie (sid=SECRET), which the worker can read and exfiltrate to the third-party iframe.
  6. Cookie Fixation (Write): A response to a worker subresource fetch with Set-Cookie: sid=ATTACKER; Secure; SameSite=None; Partitioned; Path=/ writes directly into the first-party partition (https://tracker.example, kSameSite), fixating the cookie for subsequent top-level visits.

Suggested Fix

When constructing the subresource loader factory and ContentClientParams in SharedWorkerHost, if instance_.DoesRequireCrossSiteRequestForCookies() is true, recreate the IsolationInfo with an empty net::SiteForCookies(), matching the logic used in SharedWorkerHost::ComputeIsolationInfoForWebSocket():

net::IsolationInfo isolation_info = GetWorkerStorageKey().ToPartialNetIsolationInfo();
if (instance_.DoesRequireCrossSiteRequestForCookies()) {
  isolation_info = net::IsolationInfo::Create(
      isolation_info.request_type(), *isolation_info.top_frame_origin(),
      *isolation_info.frame_origin(), net::SiteForCookies(),
      isolation_info.nonce());
}

Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c


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.

View on issue tracker