Low watch CVSS 6.5 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in WebSockets
DescriptionInsufficient validation of untrusted input in WebSockets
ComponentWebSockets
Bug ClassInput Validation
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.
Tracker483423893
Fix commite30962b8baf2 (chromium/src) +82/-170
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedRichard Belisle
Reported2026-02-10 (per vendor advisory)
Fix landed2026-02-23 (public git)
Disclosed2026-04-07
Patch gap43 days — fix public in source before the advisory

Background

`SiteForCookies`
the net primitive representing the “first party for cookies” of a request, used to decide whether SameSite cookies are sent/accepted.
`IsolationInfo`
the browser-trusted network-isolation context for a request, which itself carries a site_for_cookies() derived from the frame/worker’s actual site.
`WebSocketConnectorImpl`
the browser-process implementation of the WebSocketConnector Mojo interface the renderer calls to open a WebSocket, which then calls NetworkContext::CreateWebSocket.
`NetworkContext::CreateWebSocket`
the network-service entry point that actually initiates the WebSocket handshake with the supplied cookie/isolation parameters.

Root Cause Analysis

The WebSocketConnector::Connect Mojo method took a separate site_for_cookies argument that, for the renderer-facing WebSocketConnectorImpl::Connect, was a value “dynamically computed by the renderer” (per the commit message) and then forwarded through ConnectCalledByContentBrowserClient into NetworkContext::CreateWebSocket. Because the SiteForCookies is what gates SameSite cookie behavior, trusting a renderer-supplied value let a compromised or malicious renderer assert a first-party-for-cookies that did not match its real browser-side isolation context. The invariant violated is that cookie-scoping decisions must be based on browser-trusted state, not renderer input, especially since WebSocketConnectorImpl already held a browser-supplied isolation_info_ whose site_for_cookies() is authoritative.

The fix removes the separate site_for_cookies parameter from WebSocketConnectorImpl::Connect, ConnectCalledByContentBrowserClient, and the Mojo signature, and instead passes isolation_info_.site_for_cookies() into CreateWebSocket. Other callsites (private_ai, FIDO caBLE tunnel/authenticator, browsertests) only passed hardcoded empty SiteForCookies() or used kWebSocketOptionBlockAllCookies, so dropping the argument preserved their behavior; this is a refactor that removes a renderer-controlled input rather than reordering a validation check.

Key insight
The core mistake was sourcing the WebSocket’s SiteForCookies from a renderer-computed argument when a browser-trusted IsolationInfo with an authoritative site_for_cookies() was already available; the fix deletes the renderer-supplied parameter and derives SiteForCookies from isolation_info_ instead.

Attack Path

  1. Compromised or malicious renderer A renderer with the WebSocketConnector interface computes and supplies the site_for_cookies argument to WebSocketConnectorImpl::Connect itself.
  2. Spoof the first party for cookies It passes a SiteForCookies value that misrepresents the request’s first party, differing from the browser-side isolation_info_.
  3. Propagate to the network service Before the patch, that value flowed through ConnectCalledByContentBrowserClient into NetworkContext::CreateWebSocket, where it governs SameSite cookie attachment for the WebSocket handshake.
  4. Influence cookie scoping (inferred) This could cause SameSite cookies to be sent/accepted in a context they should not be, i.e. a cookie-policy bypass; the precise cookie behavior is not shown in the diff, so the concrete consequence is (inferred).

Impact Assessment

A renderer could supply a mismatched SiteForCookies for a WebSocket connection, potentially influencing SameSite cookie attachment for cross-site requests from a web/worker context; after the fix the browser-trusted isolation_info_.site_for_cookies() is always used. The vendor advisory rates this low (“insufficient validation of untrusted input in WebSockets”) and does not report exploitation in the wild; the cookie-policy-bypass consequence is (inferred) since the diff only removes the renderer-controlled parameter.

Files Changed

  • chrome/browser/net/websocket_browsertest.cc
  • components/private_ai/websocket_client.cc
  • content/browser/websockets/websocket_connector_impl.cc
  • content/browser/websockets/websocket_connector_impl.h
  • device/fido/cable/fido_tunnel_device.cc
  • device/fido/cable/v2_authenticator.cc

Audit Directions

  • Renderer-supplied security context
    Audit other browser-process Mojo methods that accept SiteForCookies, origins, or isolation data as explicit arguments where a browser-trusted equivalent already exists, and prefer the trusted source.
  • Redundant/divergent parameters
    Look for APIs that carry both an IsolationInfo and a separate SiteForCookies/origin that can disagree, since the extra argument is the attack surface.
  • `CreateWebSocket` callers
    Review every caller of NetworkContext::CreateWebSocket to confirm the SiteForCookies passed is consistent with the IsolationInfo and not derived from untrusted input.
From e30962b8baf2af0d8f4a09e1d71a4403801d9524 Mon Sep 17 00:00:00 2001
From: Chris Fredrickson <[email protected]>
Date: Mon, 23 Feb 2026 08:12:52 -0800
Subject: [PATCH] Remove separate SiteForCookies arg from WebSocketConnector::Connect

This removes the SiteForCookies arg from WebSocketConnector::Connect,
since the impl already has an IsolationInfo which includes a
SiteForCookies that ought to be used instead. We also remove the
associated plumbing all the way to the callsites of
NetworkContext::CreateWebSocket, and update them accordingly.

Of those callsites, all but WebSocketConnectorImpl::Connect used
hardcoded SiteForCookies values that have been preserved or were
irrelevant (due to network::mojom::kWebSocketOptionBlockAllCookies).

Previously, WebSocketConnectorImpl::Connect used a value that was
dynamically computed by the renderer. Now,
WebSocketConnectorImpl::Connect uses the SiteForCookies from the
browser-supplied IsolationInfo (from RenderFrameHostImpl,
ServiceWorkerHost, DedicatedWorkerHost, or SharedWorkerHost, as
applicable).

Fixed: 483423893
Change-Id: I556f728dfd94f516b89fcabe87c2e38b66ec31ca
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7595833
Reviewed-by: Ken Buchanan <[email protected]>
Commit-Queue: Christian Dullweber <[email protected]>
Reviewed-by: Adam Rice <[email protected]>
Auto-Submit: Chris Fredrickson <[email protected]>
Commit-Queue: Chris Fredrickson <[email protected]>
Reviewed-by: Christian Dullweber <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1588717}
---

diff --git a/chrome/browser/net/websocket_browsertest.cc b/chrome/browser/net/websocket_browsertest.cc
index 3c2c81f..ed785fc 100644
--- a/chrome/browser/net/websocket_browsertest.cc
+++ b/chrome/browser/net/websocket_browsertest.cc
@@ -162,7 +162,6 @@
     content::RenderProcessHost* const process = frame->GetProcess();
 
     const std::vector<std::string> requested_protocols;
-    const net::SiteForCookies site_for_cookies;
     // The actual value of this doesn't actually matter, it just can't be empty,
     // to avoid a DCHECK.
     const net::IsolationInfo isolation_info =
@@ -171,10 +170,10 @@
     const url::Origin origin;
 
     process->GetStoragePartition()->GetNetworkContext()->CreateWebSocket(
-        url, requested_protocols, site_for_cookies,
-        net::StorageAccessApiStatus::kNone, isolation_info,
-        std::move(additional_headers), ToOriginatingProcessId(process->GetID()),
-        origin, network::mojom::ClientSecurityState::New(),
+        url, requested_protocols, net::StorageAccessApiStatus::kNone,
+        isolation_info, std::move(additional_headers),
+        ToOriginatingProcessId(process->GetID()), origin,
+        network::mojom::ClientSecurityState::New(),
         network::mojom::kWebSocketOptionNone,
         net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS),
         std::move(handshake_client),
diff --git a/components/private_ai/websocket_client.cc b/components/private_ai/websocket_client.cc
index 27bbf8de0..34b7579e 100644
--- a/components/private_ai/websocket_client.cc
+++ b/components/private_ai/websocket_client.cc
@@ -159,8 +159,7 @@
       "X-WebChannel-Content-Type", "application/x-protobuf"));
 
   network_context_->CreateWebSocket(
-      service_url_, requested_protocols, net::SiteForCookies(),
-      net::StorageAccessApiStatus::kNone,
+      service_url_, requested_protocols, net::StorageAccessApiStatus::kNone,
       net::IsolationInfo::CreateForInternalRequest(
           url::Origin::Create(service_url_)),
       std::move(additional_headers), network::OriginatingProcessId::browser(),
diff --git a/content/browser/websockets/websocket_connector_impl.cc b/content/browser/websockets/websocket_connector_impl.cc
index 4b2ef187..7438fed 100644
--- a/content/browser/websockets/websocket_connector_impl.cc
+++ b/content/browser/websockets/websocket_connector_impl.cc
@@ -76,7 +76,6 @@
 void WebSocketConnectorImpl::Connect(
     const GURL& url,
     const std::vector<std::string>& requested_protocols,
-    const net::SiteForCookies& site_for_cookies,
     const std::optional<std::string>& user_agent,
     net::StorageAccessApiStatus storage_access_api_status,
     mojo::PendingRemote<network::mojom::WebSocketHandshakeClient>
@@ -96,11 +95,11 @@
     GetContentClient()->browser()->CreateWebSocket(
         frame,
         base::BindOnce(ConnectCalledByContentBrowserClient, requested_protocols,
-                       site_for_cookies, storage_access_api_status,
-                       isolation_info_, frame_id_, origin_,
-                       client_security_state_->Clone(), options,
+                       storage_access_api_status, isolation_info_, frame_id_,
+                       origin_, client_security_state_->Clone(), options,
                        std::move(throttling_profile_id)),
-        url, site_for_cookies, user_agent, std::move(handshake_client));
+        url, isolation_info_.site_for_cookies(), user_agent,
+        std::move(handshake_client));
     return;
   }
   std::vector<network::mojom::HttpHeaderPtr> headers;
@@ -121,9 +120,8 @@
                     frame_id_);
 
   storage_partition->GetNetworkContext()->CreateWebSocket(
-      url, requested_protocols, site_for_cookies, storage_access_api_status,
-      isolation_info_, std::move(headers),
-      ToOriginatingProcessId(frame_id_.child_id), origin_,
+      url, requested_protocols, storage_access_api_status, isolation_info_,
+      std::move(headers), ToOriginatingProcessId(frame_id_.child_id), origin_,
       client_security_state_->Clone(), options,
       net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation),
       std::move(handshake_client),
@@ -133,7 +131,6 @@
 
 void WebSocketConnectorImpl::ConnectCalledByContentBrowserClient(
     const std::vector<std::string>& requested_protocols,
-    const net::SiteForCookies& site_for_cookies,
     net::StorageAccessApiStatus storage_access_api_status,
     const net::IsolationInfo& isolation_info,
     const content::GlobalRenderFrameHostId& frame_id,
@@ -155,10 +152,9 @@
     return;
   }
   process->GetStoragePartition()->GetNetworkContext()->CreateWebSocket(
-      url, requested_protocols, site_for_cookies, storage_access_api_status,
-      isolation_info, std::move(additional_headers),
-      ToOriginatingProcessId(frame_id.child_id), origin,
-      std::move(client_security_state), options,
+      url, requested_protocols, storage_access_api_status, isolation_info,
+      std::move(additional_headers), ToOriginatingProcessId(frame_id.child_id),
+      origin, std::move(client_security_state), options,
       net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation),
       std::move(handshake_client),
       process->GetStoragePartition()->CreateURLLoaderNetworkObserverForFrame(
diff --git a/content/browser/websockets/websocket_connector_impl.h b/content/browser/websockets/websocket_connector_impl.h
index 162168e..8c13410 100644
--- a/content/browser/websockets/websocket_connector_impl.h
+++ b/content/browser/websockets/websocket_connector_impl.h
@@ -44,7 +44,6 @@
   // WebSocketConnector implementation
   void Connect(const GURL& url,
                const std::vector<std::string>& requested_protocols,
-               const net::SiteForCookies& site_for_cookies,
                const std::optional<std::string>& user_agent,
                net::StorageAccessApiStatus storage_access_api_status,
                mojo::PendingRemote<network::mojom::WebSocketHandshakeClient>
@@ -55,7 +54,6 @@
  private:
   static void ConnectCalledByContentBrowserClient(
       const std::vector<std::string>& requested_protocols,
-      const net::SiteForCookies& site_for_cookies,
       net::StorageAccessApiStatus storage_access_api_status,
       const net::IsolationInfo& isolation_info,
       const content::GlobalRenderFrameHostId& frame_id,
diff --git a/device/fido/cable/fido_tunnel_device.cc b/device/fido/cable/fido_tunnel_device.cc
index dac3d31..2acd6519 100644
--- a/device/fido/cable/fido_tunnel_device.cc
+++ b/device/fido/cable/fido_tunnel_device.cc
@@ -143,8 +143,8 @@
       base::BindRepeating(&FidoTunnelDevice::OnTunnelData,
                           base::Unretained(this)));
   network_context_factory.Run()->CreateWebSocket(
-      url, {kCableWebSocketProtocol}, net::SiteForCookies(),
-      net::StorageAccessApiStatus::kNone, net::IsolationInfo(),
+      url, {kCableWebSocketProtocol}, net::StorageAccessApiStatus::kNone,
+      net::IsolationInfo(),
       /*additional_headers=*/{}, network::OriginatingProcessId::browser(),
       url::Origin::Create(url), network::mojom::ClientSecurityState::New(),
       network::mojom::kWebSocketOptionBlockAllCookies,
@@ -201,10 +201,10 @@
   headers.emplace_back(
       network::mojom::HttpHeader::New(kCableSignalConnectionHeader, "true"));
   network_context_factory.Run()->CreateWebSocket(
-      url, {kCableWebSocketProtocol}, net::SiteForCookies(),
-      net::StorageAccessApiStatus::kNone, net::IsolationInfo(),
-      std::move(headers), network::OriginatingProcessId::browser(),
-      url::Origin::Create(url), network::mojom::ClientSecurityState::New(),
+      url, {kCableWebSocketProtocol}, net::StorageAccessApiStatus::kNone,
+      net::IsolationInfo(), std::move(headers),
+      network::OriginatingProcessId::browser(), url::Origin::Create(url),
+      network::mojom::ClientSecurityState::New(),
       network::mojom::kWebSocketOptionBlockAllCookies,
       net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation),
       websocket_client_->BindNewHandshakeClientPipe(),
diff --git a/device/fido/cable/v2_authenticator.cc b/device/fido/cable/v2_authenticator.cc
index e7abce10..7dcf5af 100644
--- a/device/fido/cable/v2_authenticator.cc
+++ b/device/fido/cable/v2_authenticator.cc
@@ -379,7 +379,7 @@
     DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
     network_context_factory_.Run()->CreateWebSocket(
-        target_, {device::kCableWebSocketProtocol}, net::SiteForCookies(),
+        target_, {device::kCableWebSocketProtocol},
         net::StorageAccessApiStatus::kNone, net::IsolationInfo(),
         /*additional_headers=*/{}, network::OriginatingProcessId::browser(),
         url::Origin::Create(target_),
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/net/websocket_browsertest.cc b/chrome/browser/net/websocket_browsertest.cc
index 3c2c81f..ed785fc 100644
--- a/chrome/browser/net/websocket_browsertest.cc
+++ b/chrome/browser/net/websocket_browsertest.cc
@@ -162,7 +162,6 @@
     content::RenderProcessHost* const process = frame->GetProcess();
 
     const std::vector<std::string> requested_protocols;
-    const net::SiteForCookies site_for_cookies;
     // The actual value of this doesn't actually matter, it just can't be empty,
     // to avoid a DCHECK.
     const net::IsolationInfo isolation_info =
@@ -171,10 +170,10 @@
     const url::Origin origin;
 
     process->GetStoragePartition()->GetNetworkContext()->CreateWebSocket(
-        url, requested_protocols, site_for_cookies,
-        net::StorageAccessApiStatus::kNone, isolation_info,
-        std::move(additional_headers), ToOriginatingProcessId(process->GetID()),
-        origin, network::mojom::ClientSecurityState::New(),
+        url, requested_protocols, net::StorageAccessApiStatus::kNone,
+        isolation_info, std::move(additional_headers),
+        ToOriginatingProcessId(process->GetID()), origin,
+        network::mojom::ClientSecurityState::New(),
         network::mojom::kWebSocketOptionNone,
         net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS),
         std::move(handshake_client),
diff --git a/net/websockets/websocket_channel_test.cc b/net/websockets/websocket_channel_test.cc
index d339cf1..ca4f651 100644
--- a/net/websockets/websocket_channel_test.cc
+++ b/net/websockets/websocket_channel_test.cc
@@ -763,7 +763,6 @@
       const GURL& new_socket_url,
       const std::vector<std::string>& requested_subprotocols,
       const url::Origin& new_origin,
-      const SiteForCookies& new_site_for_cookies,
       StorageAccessApiStatus new_storage_access_api_status,
       const IsolationInfo& new_isolation_info,
       const HttpRequestHeaders& additional_headers,
@@ -773,7 +772,6 @@
       std::unique_ptr<WebSocketStream::ConnectDelegate> new_connect_delegate) {
     socket_url = new_socket_url;
     origin = new_origin;
-    site_for_cookies = new_site_for_cookies;
     storage_access_api_status = new_storage_access_api_status;
     isolation_info = new_isolation_info;
     url_request_context = new_url_request_context;
@@ -783,7 +781,6 @@
 
   GURL socket_url;
   url::Origin origin;
-  SiteForCookies site_for_cookies;
   StorageAccessApiStatus storage_access_api_status;
   IsolationInfo isolation_info;
   raw_ptr<URLRequestContext> url_request_context;
@@ -828,9 +825,9 @@
         CreateEventInterface(), connect_data_.url_request_context.get());
     channel_->SendAddChannelRequestForTesting(
         connect_data_.socket_url, connect_data_.requested_subprotocols,
-        connect_data_.origin, connect_data_.site_for_cookies,
-        net::StorageAccessApiStatus::kNone, connect_data_.isolation_info,
-        HttpRequestHeaders(), TRAFFIC_ANNOTATION_FOR_TESTS,
+        connect_data_.origin, net::StorageAccessApiStatus::kNone,
+        connect_data_.isolation_info, HttpRequestHeaders(),
+        TRAFFIC_ANNOTATION_FOR_TESTS,
         base::BindOnce(&WebSocketStreamCreationCallbackArgumentSaver::Create,
                        base::Unretained(&connect_data_.argument_saver)));
   }
@@ -866,8 +863,7 @@
     ConnectData()
         : url_request_context(CreateTestURLRequestContextBuilder()->Build()),
           socket_url("ws://ws/"),
-          origin(url::Origin::Create(GURL("http://ws"))),
-          site_for_cookies(SiteForCookies::FromUrl(GURL("http://ws/"))) {
+          origin(url::Origin::Create(GURL("http://ws"))) {
       this->isolation_info =
           IsolationInfo::Create(IsolationInfo::RequestType::kOther, origin,
                                 origin, SiteForCookies::FromOrigin(origin));
@@ -882,8 +878,6 @@
     std::vector<std::string> requested_subprotocols;
     // Origin of the request
     url::Origin origin;
-    // First party for cookies for the request.
-    net::SiteForCookies site_for_cookies;
     // Whether the calling context has opted into the Storage Access API.
     StorageAccessApiStatus storage_access_api_status =
         StorageAccessApiStatus::kNone;
@@ -1008,8 +1002,6 @@
 TEST_F(WebSocketChannelTest, EverythingIsPassedToTheCreatorFunction) {
   connect_data_.socket_url = GURL("ws://example.com/test");
   connect_data_.origin = url::Origin::Create(GURL("http://example.com"));
-  connect_data_.site_for_cookies =
-      SiteForCookies::FromUrl(GURL("http://example.com/"));
   connect_data_.isolation_info = net::IsolationInfo::Create(
       IsolationInfo::RequestType::kOther, connect_data_.origin,
       connect_data_.origin, SiteForCookies::FromOrigin(connect_data_.origin));
@@ -1025,8 +1017,6 @@
 
   EXPECT_EQ(connect_data_.socket_url, actual.socket_url);
   EXPECT_EQ(connect_data_.origin.Serialize(), actual.origin.Serialize());
-  EXPECT_TRUE(
-      connect_data_.site_for_cookies.IsEquivalent(actual.site_for_cookies));
   EXPECT_EQ(connect_data_.storage_access_api_status,
             actual.storage_access_api_status);
   EXPECT_TRUE(
diff --git a/net/websockets/websocket_end_to_end_test.cc b/net/websockets/websocket_end_to_end_test.cc
index 3a56887..f95c0d7 100644
--- a/net/websockets/websocket_end_to_end_test.cc
+++ b/net/websockets/websocket_end_to_end_test.cc
@@ -419,8 +419,6 @@
       InitialiseContext();
     }
     url::Origin origin = url::Origin::Create(GURL("http://localhost"));
-    net::SiteForCookies site_for_cookies =
-        net::SiteForCookies::FromOrigin(origin);
     IsolationInfo isolation_info =
         IsolationInfo::Create(IsolationInfo::RequestType::kOther, origin,
                               origin, SiteForCookies::FromOrigin(origin));
@@ -428,9 +426,8 @@
     channel_ = std::make_unique<WebSocketChannel>(std::move(event_interface),
                                                   context_.get());
     channel_->SendAddChannelRequest(
-        GURL(socket_url), sub_protocols_, origin, site_for_cookies,
-        StorageAccessApiStatus::kNone, isolation_info, HttpRequestHeaders(),
-        TRAFFIC_ANNOTATION_FOR_TESTS);
+        GURL(socket_url), sub_protocols_, origin, StorageAccessApiStatus::kNone,
+        isolation_info, HttpRequestHeaders(), TRAFFIC_ANNOTATION_FOR_TESTS);
   }
 
   // Send the connect request to |socket_url| and wait for a response. Returns
diff --git a/net/websockets/websocket_stream_cookie_test.cc b/net/websockets/websocket_stream_cookie_test.cc
index 99b0eed..bcfc4d8 100644
--- a/net/websockets/websocket_stream_cookie_test.cc
+++ b/net/websockets/websocket_stream_cookie_test.cc
@@ -54,7 +54,6 @@
  public:
   void CreateAndConnect(const GURL& url,
                         const url::Origin& origin,
-                        const SiteForCookies& site_for_cookies,
                         const IsolationInfo& isolation_info,
                         const WebSocketExtraHeaders& cookie_header,
                         const std::string& response_body) {
@@ -63,7 +62,7 @@
             url.GetPath(), url.GetHost(), origin, cookie_header,
             /*send_additional_request_headers=*/{}, /*extra_headers=*/{}),
         response_body);
-    CreateAndConnectStream(url, NoSubProtocols(), origin, site_for_cookies,
+    CreateAndConnectStream(url, NoSubProtocols(), origin,
                            StorageAccessApiStatus::kNone, isolation_info,
                            HttpRequestHeaders(), nullptr);
   }
@@ -148,7 +147,6 @@
   const GURL url(GetParam().url);
   const GURL cookie_url(GetParam().cookie_url);
   const url::Origin origin = url::Origin::Create(GURL(GetParam().url));
-  const SiteForCookies site_for_cookies = SiteForCookies::FromOrigin(origin);
   const IsolationInfo isolation_info =
       IsolationInfo::Create(IsolationInfo::RequestType::kOther, origin, origin,
                             SiteForCookies::FromOrigin(origin));
@@ -171,8 +169,8 @@
   ASSERT_TRUE(is_called);
   ASSERT_TRUE(set_cookie_result);
 
-  CreateAndConnect(url, origin, site_for_cookies, isolation_info,
-                   GetParam().cookie_header, WebSocketStandardResponse(""));
+  CreateAndConnect(url, origin, isolation_info, GetParam().cookie_header,
+                   WebSocketStandardResponse(""));
   WaitUntilConnectDone();
   EXPECT_FALSE(has_failed());
 }
@@ -185,7 +183,6 @@
   const GURL url(GetParam().url);
   const GURL cookie_url(GetParam().cookie_url);
   const url::Origin origin = url::Origin::Create(GURL(GetParam().url));
-  const SiteForCookies site_for_cookies = SiteForCookies::FromOrigin(origin);
   const IsolationInfo isolation_info =
       IsolationInfo::Create(IsolationInfo::RequestType::kOther, origin, origin,
                             SiteForCookies::FromOrigin(origin));
@@ -203,8 +200,7 @@
   CookieStore* store =
       url_request_context_host_.GetURLRequestContext()->cookie_store();
 
-  CreateAndConnect(url, origin, site_for_cookies, isolation_info,
-                   /*cookie_header=*/{}, response);
+  CreateAndConnect(url, origin, isolation_info, /*cookie_header=*/{}, response);
   WaitUntilConnectDone();
   EXPECT_FALSE(has_failed()) << failure_message();
diff --git a/net/websockets/websocket_stream_test.cc b/net/websockets/websocket_stream_test.cc
index 7915234..1ba83c3 100644
--- a/net/websockets/websocket_stream_test.cc
+++ b/net/websockets/websocket_stream_test.cc
@@ -112,10 +112,6 @@
   return url::Origin::Create(GURL(kOrigin));
 }
 
-static net::SiteForCookies SiteForCookies() {
-  return net::SiteForCookies::FromOrigin(Origin());
-}
-
 static IsolationInfo CreateIsolationInfo() {
   url::Origin origin = Origin();
   return IsolationInfo::Create(IsolationInfo::RequestType::kOther, origin,
@@ -196,8 +192,7 @@
               WebSocketExtraHeadersToString(extra_response_headers)) +
               additional_data_);
       CreateAndConnectStream(socket_url, sub_protocols, Origin(),
-                             SiteForCookies(), storage_access_api_status,
-                             CreateIsolationInfo(),
+                             storage_access_api_status, CreateIsolationInfo(),
                              WebSocketExtraHeadersToHttpRequestHeaders(
                                  send_additional_request_headers),
                              std::move(timer_));
@@ -331,8 +326,7 @@
     EXPECT_FALSE(request->is_pending());
 
     CreateAndConnectStream(socket_url, sub_protocols, Origin(),
-                           SiteForCookies(), storage_access_api_status,
-                           CreateIsolationInfo(),
+                           storage_access_api_status, CreateIsolationInfo(),
                            WebSocketExtraHeadersToHttpRequestHeaders(
                                send_additional_request_headers),
                            std::move(timer_));
@@ -360,8 +354,7 @@
                                  extra_request_headers),
         response_body);
     CreateAndConnectStream(socket_url, sub_protocols, Origin(),
-                           SiteForCookies(), storage_access_api_status,
-                           CreateIsolationInfo(),
+                           storage_access_api_status, CreateIsolationInfo(),
                            WebSocketExtraHeadersToHttpRequestHeaders(
                                send_additional_request_headers),
                            nullptr);
@@ -388,9 +381,8 @@
                                  /*extra_headers=*/{}),
         WebSocketStandardResponse(extra_response_headers));
     CreateAndConnectStream(socket_url, sub_protocols, Origin(),
-                           SiteForCookies(), storage_access_api_status,
-                           CreateIsolationInfo(), HttpRequestHeaders(),
-                           nullptr);
+                           storage_access_api_status, CreateIsolationInfo(),
+                           HttpRequestHeaders(), nullptr);
   }
 
   // Like CreateAndConnectStandard(), but take raw mock data.
@@ -404,7 +396,7 @@
     ASSERT_EQ(BASIC_HANDSHAKE_STREAM, stream_type_);
 
     AddRawExpectations(std::move(socket_data));
-    CreateAndConnectStream(GURL(url), sub_protocols, Origin(), SiteForCookies(),
+    CreateAndConnectStream(GURL(url), sub_protocols, Origin(),
                            storage_access_api_status, CreateIsolationInfo(),
                            additional_headers, std::move(timer_));
   }
diff --git a/services/network/test/test_network_context.h b/services/network/test/test_network_context.h
index 278c242e..e561dcc 100644
--- a/services/network/test/test_network_context.h
+++ b/services/network/test/test_network_context.h
@@ -198,7 +198,6 @@
   void CreateWebSocket(
       const GURL& url,
       const std::vector<std::string>& requested_protocols,
-      const net::SiteForCookies& site_for_cookies,
       net::StorageAccessApiStatus storage_access_api_status,
       const net::IsolationInfo& isolation_info,
       std::vector<mojom::HttpHeaderPtr> additional_headers,
diff --git a/services/network/websocket_factory_unittest.cc b/services/network/websocket_factory_unittest.cc
index 2732347..31e0099 100644
--- a/services/network/websocket_factory_unittest.cc
+++ b/services/network/websocket_factory_unittest.cc
@@ -83,9 +83,9 @@
     // WebSocket objects are owned by the factory and will be deleted
     // asynchronously.
     factory_->CreateWebSocket(
-        url, requested_protocols, net::SiteForCookies(),
-        net::StorageAccessApiStatus::kNone, net::IsolationInfo(), {},
-        network::OriginatingProcessId::browser(), url::Origin::Create(url),
+        url, requested_protocols, net::StorageAccessApiStatus::kNone,
+        net::IsolationInfo(), {}, network::OriginatingProcessId::browser(),
+        url::Origin::Create(url),
         /*client_security_state=*/nullptr, /*options=*/0,
         TRAFFIC_ANNOTATION_FOR_TESTS, std::move(handshake_client),
         mojo::NullRemote(), mojo::NullRemote(), mojo::NullRemote(),
diff --git a/third_party/blink/renderer/modules/websockets/websocket_channel_impl_test.cc b/third_party/blink/renderer/modules/websockets/websocket_channel_impl_test.cc
index adb2bed7..0ff005f 100644
--- a/third_party/blink/renderer/modules/websockets/websocket_channel_impl_test.cc
+++ b/third_party/blink/renderer/modules/websockets/websocket_channel_impl_test.cc
@@ -198,19 +198,16 @@
       ConnectArgs(
           const KURL& url,
           const Vector<String>& protocols,
-          const net::SiteForCookies& site_for_cookies,
           const String& user_agent,
           mojo::PendingRemote<network::mojom::blink::WebSocketHandshakeClient>
               handshake_client)
           : url(url),
             protocols(protocols),
-            site_for_cookies(site_for_cookies),
             user_agent(user_agent),
             handshake_client(std::move(handshake_client)) {}
 
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

WebSocketConnector.Connect() passes renderer-controlled site_for_cookies and storage_access_api_status unvalidated, enabling third-party cookie blocking bypass

WebSocketConnector.Connect() takes site_for_cookies and storage_access_api_status as renderer-supplied Mojo parameters. Neither is validated against browser-side state before being passed through to the network stack.

Data flow (zero validation at every hop): Renderer → WebSocketConnectorImpl::Connect() (websocket_connector_impl.cc:123) → NetworkContext::CreateWebSocket() (network_context.cc:1948) → websocket_stream.cc:143 url_request_->set_site_for_cookies(site_for_cookies) → websocket_stream.cc:148-154 sets kStorageAccessGrantEligible override

A compromised renderer spoofs site_for_cookies to the target origin and storage_access_api_status to kAccessViaAPI, then opens a cross-site WebSocket. The network stack treats the connection as first-party with a Storage Access grant, bypassing third-party cookie blocking. SameSite=None cookies from the target are included in the handshake.

SameSite=Lax/Strict cookies are NOT affected — the browser-controlled initiator_origin still reflects the true cross-site context.

The codebase already has a browser-controlled alternative ready: network_context.mojom:1409-1411: “Currently, |isolation_info|’s SiteForCookies field is ignored, but it will eventually replace the |site_for_cookies| parameter.”

Fix: use isolation_info.SiteForCookies() and derive storage_access_api_status from browser-side permission state.

Related: RestrictedCookieManager::ValidateAccessToCookiesAt() has the same trust issue — site_for_cookies mismatches are LOG(ERROR) only per crbug.com/402207912.

VERSION Chrome Version: 143.0.7499.192 stable Operating System: Linux (Kali 6.18.3) — bug is platform-independent

REPRODUCTION CASE See attached ws-cookie-bypass-poc.py.

Prerequisites:

  • Chromium 143+ installed (or set CHROMIUM=/path/to/chrome)
  • openssl CLI (for cert generation)
  • Python 3 + websockets (pip install websockets)
  • /etc/hosts: 127.0.0.1 example.test target-domain.example
  • Run: python3 ws-cookie-bypass-poc.py

Output: Phase 1 (control, unmodified forward): 0 cookies — correct Phase 2 (spoofed SFC + SAA status): session_none=LEAKED_NONE — bypass

The PoC uses MojoInterfaceInterceptor to capture a real Connect() message, modifies site_for_cookies and storage_access_api_status in-place, and forwards through a new WebSocketConnector pipe. Requires –enable-blink-features=MojoJS to simulate compromised renderer Mojo access.

Reporter Credit: Richard Belisle

View on issue tracker