Released June 29, 2026

CVE-2026-43708

CVSS 4.3 severity medium CWE-20 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N

Overview

Impact:

Description: A malicious website may exfiltrate data cross-origin

Researchers: Behzad Najjarpour Jabbari (@G4ru)

Key insight. The partitioned-cookie guard tested a lazily-created request that could be nil, so -respondsToSelector: returned NO and WebSocket requests silently skipped third-party cookie blocking; materializing the request via ensureMutableRequest() applies the policy.

Patch Details

diff --git a/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm b/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm
index 7fc3aaf13384..e9ab1470b2e7 100644
--- a/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm
+++ b/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm
@@ -1693,7 +1693,7 @@ RefPtr<WebSocketTask> NetworkSessionCocoa::createWebSocketTask(WebPageProxyIdent
         ensureMutableRequest().get()._privacyProxyFailClosedForUnreachableNonMainHosts = YES;
 
 #if ENABLE(OPT_IN_PARTITIONED_COOKIES) && defined(CFN_COOKIE_ACCEPTS_POLICY_PARTITION) && CFN_COOKIE_ACCEPTS_POLICY_PARTITION
-    if ([mutableRequest respondsToSelector:@selector(_setAllowOnlyPartitionedCookies:)]) {
+    if ([ensureMutableRequest() respondsToSelector:@selector(_setAllowOnlyPartitionedCookies:)]) {
         if (CheckedPtr storageSession = networkStorageSession(); storageSession && storageSession->isOptInCookiePartitioningEnabled()) {
             bool shouldAllowOnlyPartitioned = storageSession->thirdPartyCookieBlockingDecisionForRequest(request, frameID, pageID, networkProcess().shouldRelaxThirdPartyCookieBlockingForPage(webPageProxyID), isRequestToKnownCrossSiteTracker(request)) == WebCore::ThirdPartyCookieBlockingDecision::AllExceptPartitioned;
             [mutableRequest _setAllowOnlyPartitionedCookies:shouldAllowOnlyPartitioned];
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm
index 0f29bd2dfe83..8e19192730f0 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm
@@ -836,6 +836,50 @@ TEST(WKHTTPCookieStore, DISABLED_WebSocketCookies)
     Util::run(&receivedThirdRequest);
 }
 
+TEST(WKHTTPCookieStore, WebSocketCookies2)
+{
+    using namespace TestWebKitAPI;
+    bool receivedThirdRequest { false };
+    HTTPServer server(TestWebKitAPI::HTTPServer::UseCoroutines::Yes, [&] (Connection connection) -> ConnectionTask { while (true) {
+        auto request = co_await connection.awaitableReceiveHTTPRequest();
+        auto path = HTTPServer::parsePath(request);
+        request.append(0);
+        if (path == "http://sitea.example/com"_s) {
+            co_await connection.awaitableSend(
+                "HTTP/1.1 200 OK\r\n"
+                "Content-Length: 0\r\n"
+                "Set-Cookie: Default=1\r\n"
+                "Set-Cookie: SameSite_None=1; SameSite=None\r\n"
+                "Set-Cookie: SameSite_None_Secure=1; secure; SameSite=None\r\n"
+                "Set-Cookie: SameSite_Lax=1; SameSite=Lax\r\n"
+                "Set-Cookie: SameSite_Strict=1; SameSite=Strict\r\n"
+                "\r\n"_s);
+        } else if (path == "ws://sitea.example/websocket"_s) {
+            EXPECT_TRUE(contains(request.span(), "Host: sitea.example"_span));
+            EXPECT_FALSE(contains(request.span(), "Cookie:"_span));
+            receivedThirdRequest = true;
+        } else if (path == "http://siteb.example/ninja"_s) {
+            auto html = @"<script>new WebSocket('ws://siteA.example/websocket')</script>";
+            co_await connection.awaitableSend(HTTPResponse(html).serialize());
+        } else
+            EXPECT_WK_STREQ(@"SHOULD NOT BE REACH", path);
+    } });
+
+    RetainPtr storeConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration]);
+    [storeConfiguration setHTTPProxy:[NSURL URLWithString:[NSString stringWithFormat:@"https://127.0.0.1:%d/", server.port()]]];
+    RetainPtr dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration.get()]);
+    RetainPtr viewConfiguration = adoptNS([WKWebViewConfiguration new]);
+    [viewConfiguration setWebsiteDataStore:dataStore.get()];
+
+    RetainPtr webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:viewConfiguration.get()]);
+    [[[webView configuration] websiteDataStore] _setResourceLoadStatisticsEnabled:YES];
+    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://siteA.example/com"]]];
+    [webView _test_waitForDidFinishNavigation];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://siteB.example/ninja"]]];
+    Util::run(&receivedThirdRequest);
+}
+
 TEST(WKHTTPCookieStore, WebSocketCookiesFromRedirect)
 {
     using namespace TestWebKitAPI;

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-43708
Bugzilla 315306
Component WebKit
Bug Class CrossOrigin
Severity medium
Commit 971435fdd3868e7f…
Advisory Apple Advisory

Terminology

  • Partitioned cookies — Opt-in cookie partitioning isolates third-party cookies per top-level site to prevent cross-site tracking/leakage.
  • respondsToSelector on nil — Objective-C sends to nil are no-ops and -respondsToSelector: on nil returns NO, so a nil-guarded block silently does nothing.
  • ensureMutableRequest — Lazily materializes the mutable NSURLRequest; without calling it the request may be nil when checked.

Root Cause Analysis

This fixes a third-party cookie partitioning bypass on WebSocket requests that let cookies leak cross-site. In NetworkSessionCocoa::createWebSocketTask, the code that applies opt-in cookie partitioning was guarded by [mutableRequest respondsToSelector:@selector(_setAllowOnlyPartitionedCookies:)]. mutableRequest is produced lazily via ensureMutableRequest(); at that point it could be nil/unmaterialized, and -respondsToSelector: on nil returns NO, so the entire block that computes the third-party cookie blocking decision and calls _setAllowOnlyPartitionedCookies: was skipped. As a result, a cross-site WebSocket did not have partitioned-cookie / third-party-cookie-blocking policy applied, so it could carry the user’s full (unpartitioned) first-party cookies to a third-party host — cross-site data exfiltration / tracking. The fix changes the guard to [ensureMutableRequest() respondsToSelector:…], forcing the mutable request to be materialized so the selector check succeeds and the partitioning decision (thirdPartyCookieBlockingDecisionForRequest … AllExceptPartitioned) is actually applied via _setAllowOnlyPartitionedCookies:. The restored invariant is that WebSocket requests receive the same third-party cookie partitioning/blocking decision as other requests. The regression test loads siteB, which opens a WebSocket to siteA, and asserts the WebSocket handshake to siteA carries NO Cookie header (third-party cookies blocked/partitioned).

Attack Path

1. Set cookies on the target

The user visits siteA which sets cookies (including SameSite=None).

2. Open a cross-site WebSocket

From attacker siteB, script opens a WebSocket to siteA (a third-party context).

3. Skip partitioning

Because the guard tested a nil mutableRequest, the partitioned-cookie policy is never applied to the WebSocket.

4. Leak cookies cross-site

The WebSocket handshake carries siteA’s unpartitioned cookies to the third-party connection, exfiltrating them cross-origin.

Impact Assessment

A cross-site information-disclosure issue in the Network process: WebSocket requests bypassed third-party cookie partitioning/blocking and could carry unpartitioned cookies to third parties, enabling cross-site tracking and cookie exfiltration. No memory corruption; the impact is privacy/data leakage across origins.

Audit Directions

Same file: nil-guarded policy blocks

Grep NetworkSessionCocoa.mm for respondsToSelector: on mutableRequest/lazy objects guarding security policy application; ensure the object is materialized first.

WebSocket vs HTTP parity

Compare createWebSocketTask against the HTTP request path to confirm cookie partitioning, privacy proxy, and tracker-blocking decisions are applied identically.

Changed Functions

Function File Change Note
NetworkSessionCocoa::createWebSocketTask Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm modified Uses ensureMutableRequest() (materializing the request) in the respondsToSelector guard so _setAllowOnlyPartitionedCookies: and the third-party cookie blocking decision are actually applied to WebSocket requests.

Files Changed

Source Files

  • Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm

Test Files

  • Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm

Regression Test

The fix ships this proof-of-concept / regression test:

diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm
index 0f29bd2dfe83..8e19192730f0 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm
@@ -836,6 +836,50 @@ TEST(WKHTTPCookieStore, DISABLED_WebSocketCookies)
     Util::run(&receivedThirdRequest);
 }
 
+TEST(WKHTTPCookieStore, WebSocketCookies2)
+{
+    using namespace TestWebKitAPI;
+    bool receivedThirdRequest { false };
+    HTTPServer server(TestWebKitAPI::HTTPServer::UseCoroutines::Yes, [&] (Connection connection) -> ConnectionTask { while (true) {
+        auto request = co_await connection.awaitableReceiveHTTPRequest();
+        auto path = HTTPServer::parsePath(request);
+        request.append(0);
+        if (path == "http://sitea.example/com"_s) {
+            co_await connection.awaitableSend(
+                "HTTP/1.1 200 OK\r\n"
+                "Content-Length: 0\r\n"
+                "Set-Cookie: Default=1\r\n"
+                "Set-Cookie: SameSite_None=1; SameSite=None\r\n"
+                "Set-Cookie: SameSite_None_Secure=1; secure; SameSite=None\r\n"
+                "Set-Cookie: SameSite_Lax=1; SameSite=Lax\r\n"
+                "Set-Cookie: SameSite_Strict=1; SameSite=Strict\r\n"
+                "\r\n"_s);
+        } else if (path == "ws://sitea.example/websocket"_s) {
+            EXPECT_TRUE(contains(request.span(), "Host: sitea.example"_span));
+            EXPECT_FALSE(contains(request.span(), "Cookie:"_span));
+            receivedThirdRequest = true;
+        } else if (path == "http://siteb.example/ninja"_s) {
+            auto html = @"<script>new WebSocket('ws://siteA.example/websocket')</script>";
+            co_await connection.awaitableSend(HTTPResponse(html).serialize());
+        } else
+            EXPECT_WK_STREQ(@"SHOULD NOT BE REACH", path);
+    } });
+
+    RetainPtr storeConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration]);
+    [storeConfiguration setHTTPProxy:[NSURL URLWithString:[NSString stringWithFormat:@"https://127.0.0.1:%d/", server.port()]]];
+    RetainPtr dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration.get()]);
+    RetainPtr viewConfiguration = adoptNS([WKWebViewConfiguration new]);
+    [viewConfiguration setWebsiteDataStore:dataStore.get()];
+
+    RetainPtr webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:viewConfiguration.get()]);
+    [[[webView configuration] websiteDataStore] _setResourceLoadStatisticsEnabled:YES];
+    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://siteA.example/com"]]];
+    [webView _test_waitForDidFinishNavigation];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://siteB.example/ninja"]]];
+    Util::run(&receivedThirdRequest);
+}
+
 TEST(WKHTTPCookieStore, WebSocketCookiesFromRedirect)
 {
     using namespace TestWebKitAPI;