← All issues

[6] DedicatedWorker WebSocket handshake skips ITP third-party cookie blocking

Severity: Low | Component: WebCore/WebKit WebSocket stack and NetworkProcess ITP | a6b2961

Rated Low because the diff closes a privacy gap where a DedicatedWorker WebSocket handshake carried third-party cookies that ITP intends to strip; the impact is cross-site tracking/identity correlation reachable from ordinary web content, not memory corruption or cross-origin script access, and it only matters when third-party cookie blocking is enabled.

DedicatedWorker WebSocket connections were not subjected to ITP third-party cookie blocking, unlike DedicatedWorker fetch requests which already carried isInitiatedByDedicatedWorker through NetworkResourceLoadParameters. The patch threads a new isInitiatedByDedicatedWorker flag from the worker thread through WorkerThreadableWebSocketChannel::Bridge::initialize() — where it is derived via is<DedicatedWorkerGlobalScope>(scope) — into the WebSocketTaskCocoa constructor, where it replaces the previous shouldBlockCookies() call with thirdPartyCookieBlockingDecisionForRequest(..., isInitiatedByDedicatedWorker), so the same policy applied to worker fetch requests now applies to worker WebSocket handshakes.

Source/WebCore/Modules/websockets/WorkerThreadableWebSocketChannel.cpp

void WorkerThreadableWebSocketChannel::Bridge::initialize(WorkerGlobalScope& scope)
{
...
- m_loaderProxy->postTaskToLoader([&semaphore, &peer, ..., provider = m_socketProvider](ScriptExecutionContext& context) mutable {
- peer = mainThreadInitialize(context, workerThread.get(), workerContextIdentifier, WTF::move(workerClientWrapper), taskMode, WTF::move(provider));
+ auto isInitiatedByDedicatedWorker = is<DedicatedWorkerGlobalScope>(scope) ? IsInitiatedByDedicatedWorker::Yes : IsInitiatedByDedicatedWorker::No;
+ m_loaderProxy->postTaskToLoader([&semaphore, &peer, ..., provider = m_socketProvider, isInitiatedByDedicatedWorker](ScriptExecutionContext& context) mutable {
+ peer = mainThreadInitialize(context, workerThread.get(), workerContextIdentifier, WTF::move(workerClientWrapper), taskMode, WTF::move(provider), isInitiatedByDedicatedWorker);
semaphore.signal();
});

Source/WebKit/NetworkProcess/cocoa/WebSocketTaskCocoa.mm

- // previously: shouldBlockCookies(...)
+ // now: thirdPartyCookieBlockingDecisionForRequest(..., isInitiatedByDedicatedWorker)

LayoutTests/http/tests/websocket/tests/hybi/resources/websocket-blocked-sending-cookie-as-third-party-worker.js

+let ws = new WebSocket("ws://localhost:8880/websocket/tests/hybi/websocket-blocked-sending-cookie-as-third-party");
+ws.onopen = () => { /* PASS: request did not contain cookies */ };
+ws.onerror = () => { /* FAIL: request contained cookies */ };

The patch threads a new IsInitiatedByDedicatedWorker enum (enum class IsInitiatedByDedicatedWorker : bool { No, Yes }) from the worker thread down to the Cocoa network WebSocket task. In Bridge::initialize() the flag is derived via is<DedicatedWorkerGlobalScope>(scope) and captured into the cross-thread task posted to the loader. It is passed through mainThreadInitialize()Peer::create()/Peer::Peer()ThreadableWebSocketChannel::create(Document&, ...)SocketProvider::createWebSocketChannel(). On the WebKit side the same parameter is added to the CreateSocketChannel IPC message and threaded through NetworkConnectionToWebProcess::createSocketChannel(), NetworkSocketChannel, NetworkSession::createWebSocketTask(), and NetworkSessionCocoa::createWebSocketTask()WebSocketTask. The load-bearing change is in the WebSocketTask (Cocoa) constructor, where the unconditional shouldBlockCookies() call is replaced with thirdPartyCookieBlockingDecisionForRequest(..., isInitiatedByDedicatedWorker). The bulk of the diff is mechanical signature plumbing across Curl/Soup/Legacy/Empty implementations.

Inconsistent enforcement of a privacy policy across sibling request paths: the worker-origin signal that gates third-party cookie blocking was carried for fetch but dropped for WebSocket handshakes.

ITP (Intelligent Tracking Prevention) is a WebKit privacy feature that, among other things, blocks cookies on cross-site (third-party) requests for tracker-classified domains or when third-party cookie blocking is enabled. A DedicatedWorker is a worker tied to a single owning document; is<DedicatedWorkerGlobalScope>(scope) distinguishes it from shared/service workers. A WebSocket handshake is the initial HTTP Upgrade request a new WebSocket(url) issues; like any HTTP request it can carry cookies for the target origin. Because workers run off the main thread, WebSocket setup is marshalled to the main/loader thread via Bridge::initialize()postTaskToLoadermainThreadInitialize, which builds a main-thread Peer that owns the real ThreadableWebSocketChannel. thirdPartyCookieBlockingDecisionForRequest and shouldBlockCookies are two NetworkProcess decision routines for whether a request should send cookies; the former takes the worker-origination signal as input so it can apply the same policy used for worker fetches.

This is a logic error / privacy-policy bypass — an ITP third-party cookie-blocking gap, not a memory-safety issue. Before the fix, the WebSocket connection path originating from a DedicatedWorker did not propagate the fact that the request was worker-initiated down to the network layer. The Cocoa WebSocketTask constructor decided third-party cookie handling with a plain shouldBlockCookies() call that lacked the worker context the equivalent fetch path already carried.

🔒

How an ITP privacy boundary that held for one request type silently failed for another, and what an embedded third party could observe before the fix.

Subscribe to read more

🔒

Three reusable audit directions covering parallel request paths and cross-thread provenance loss in the worker networking stack, with concrete starting call sites.

Subscribe to read more