Medium CVSS 6.5 webkit Logic Error 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionA maliciously crafted webpage may be able to fingerprint the user
ComponentWebCore
Sub-componentWebCore Workers
Bug ClassLogic Error
Tracker271159
Fix commitc33df2d32360 (WebKit/WebKit) +215/-41
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedJoe Rutkowski (@Joe12387) of Crawless and @abrahamjuliot
Disclosed2024-05-13

Background

Canvas fingerprinting
Deriving a stable identifier from subtle per-device/OS differences in canvas/OffscreenCanvas pixel output.
Noise injection (Advanced Privacy Protections)
In Private Browsing WebKit perturbs canvas readback using a per-origin hash salt so the pixels cannot be used as a fingerprint.
Shared/service workers and remote Page
These workers run in a separate, potentially-remote Page; state like the noise salt must be explicitly plumbed to them or it is absent.
WorkerParameters plumbing
The mechanism that carries context state (including privacy flags/salt) from the originating document to a worker global scope.

Root Cause Analysis

This fixes a canvas anti-fingerprinting gap: in Private Browsing, pixel-readback noise injection was not applied to OffscreenCanvas used inside shared workers and service workers. In Private Browsing (Advanced Privacy Protections), each ScriptExecutionContext carries a per-origin noise-injection hash salt and AdvancedPrivacyProtections flags, sourced from the document loader, and these are used to perturb pixels read back from canvas/OffscreenCanvas so the output cannot be used as a stable fingerprint. For dedicated workers the salt/flags were already plumbed via WorkerParameters to the WorkerGlobalScope, so OffscreenCanvas there got noise. But shared workers and service workers run their OffscreenCanvas code in a separate, potentially-remote Page that had neither the hash salt nor the AdvancedPrivacyProtections flags, so readback in those contexts received NO noise — letting a site fingerprint the device/OS via OffscreenCanvas from a shared/service worker, bypassing the Private Browsing protection.

The fix extends the AdvancedPrivacyProtections plumbing to those two worker types: it adds an override point (ScriptExecutionContext::advancedPrivacyProtections, implemented by Document via the top document’s loader and by workers via passed-in parameters), threads the flags through WorkerScriptLoader / WorkerInitializationData / WorkerParameters, and lets Page::setupForRemoteWorker receive the privacy protections when initializing the remote Page.

The restored invariant is that canvas/OffscreenCanvas readback noise is applied in all worker types (dedicated, shared, service) when Private Browsing protections are active. The test verifies noise injection for OffscreenCanvas in a shared worker.

Key insight
The canvas noise-injection salt and privacy flags were plumbed to dedicated workers but not to shared/service workers, whose remote Page had neither, so OffscreenCanvas readback there was un-noised; extending the plumbing restores noise in all worker types.

Attack Path

  1. Enter Private Browsing The user browses privately, expecting canvas readback to be noised against fingerprinting.
  2. Use OffscreenCanvas in a shared/service worker The page renders to an OffscreenCanvas inside a shared or service worker, whose remote Page lacked the salt and privacy flags.
  3. Read back un-noised pixels Because noise injection was not applied in that context, the readback returns the true, un-perturbed pixels.
  4. Fingerprint the device Derive a stable device/OS fingerprint from the un-noised output, defeating the Private Browsing protection.

Impact Assessment

A privacy/anti-fingerprinting gap in the WebContent process with no memory corruption: OffscreenCanvas readback in shared/service workers escaped Private Browsing noise injection, allowing device/OS fingerprinting that the protection is meant to prevent. The risk is tracking/deanonymization of Private Browsing users, not code execution.

Changed Functions

FunctionChangeNotes
ServiceWorkerContainer
Source/WebCore/dom/ScriptExecutionContext.h
modified
SocketProvider
Source/WebCore/dom/ScriptExecutionContext.h
modified
WebCoreOpaqueRoot
Source/WebCore/dom/ScriptExecutionContext.h
modified
WorkerOrWorkletScriptController
Source/WebCore/workers/WorkerOrWorkletGlobalScope.h
modified
WorkerOrWorkletThread
Source/WebCore/workers/WorkerOrWorkletGlobalScope.h
modified
WorkerOrWorkletGlobalScope
Source/WebCore/workers/WorkerOrWorkletGlobalScope.h
modified
if
Source/WebCore/workers/WorkerScriptLoader.cpp
modified
IDBConnectionProxy
Source/WebCore/workers/WorkerThread.h
modified

Files Changed

  • Source/WebCore/Modules/webaudio/AudioWorkletMessagingProxy.cpp
  • Source/WebCore/dom/Document.cpp
  • Source/WebCore/dom/Document.h
  • Source/WebCore/dom/EmptyScriptExecutionContext.h
  • Source/WebCore/dom/ScriptExecutionContext.h
  • Source/WebCore/page/Page.cpp
  • Source/WebCore/page/Page.h
  • Source/WebCore/workers/Worker.cpp
  • Source/WebCore/workers/WorkerGlobalScope.cpp
  • Source/WebCore/workers/WorkerInitializationData.h
  • Source/WebCore/workers/WorkerMessagingProxy.cpp
  • Source/WebCore/workers/WorkerOrWorkletGlobalScope.cpp
  • Source/WebCore/workers/WorkerOrWorkletGlobalScope.h
  • Source/WebCore/workers/WorkerScriptLoader.cpp
  • Source/WebCore/workers/WorkerScriptLoader.h
  • Source/WebCore/workers/WorkerThread.cpp
  • Source/WebCore/workers/WorkerThread.h
  • Source/WebCore/workers/service/ServiceWorkerClientData.cpp
  • Source/WebCore/workers/service/ServiceWorkerClientData.h
  • Source/WebCore/workers/service/context/ServiceWorkerThread.cpp
  • Source/WebCore/workers/service/context/ServiceWorkerThread.h
  • Source/WebCore/workers/service/context/ServiceWorkerThreadProxy.cpp
  • Source/WebCore/workers/service/server/SWServer.cpp
  • Source/WebCore/workers/service/server/SWServer.h
  • Source/WebCore/workers/service/server/SWServerToContextConnection.h
  • Source/WebCore/workers/shared/SharedWorkerScriptLoader.cpp
  • Source/WebCore/workers/shared/context/SharedWorkerThreadProxy.cpp
  • Source/WebCore/worklets/WorkletGlobalScope.cpp
  • Source/WebCore/worklets/WorkletParameters.h
  • Source/WebKit/NetworkProcess/ServiceWorker/WebSWServerConnection.cpp
  • Source/WebKit/NetworkProcess/ServiceWorker/WebSWServerToContextConnection.cpp
  • Source/WebKit/NetworkProcess/ServiceWorker/WebSWServerToContextConnection.h
  • Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in
  • Source/WebKit/WebProcess/Storage/WebSWContextManagerConnection.cpp
  • Source/WebKit/WebProcess/Storage/WebSWContextManagerConnection.h
  • Source/WebKit/WebProcess/Storage/WebSWContextManagerConnection.messages.in
  • Source/WebKit/WebProcess/Storage/WebSharedWorkerContextManagerConnection.cpp
  • Tools/TestWebKitAPI/Tests/WebKit/AdvancedPrivacyProtections.mm

Audit Directions

  • Privacy-flag plumbing across contexts
    Audit every ScriptExecutionContext subtype (Document, dedicated/shared/service workers, worklets) for consistent propagation of the noise salt and AdvancedPrivacyProtections used by canvas/OffscreenCanvas.
  • Remote-Page state gaps
    Grep Page::setupForRemoteWorker and remote-worker init for other per-origin privacy/security state that a remote Page may be missing.
diff --git a/Source/WebCore/Modules/webaudio/AudioWorkletMessagingProxy.cpp b/Source/WebCore/Modules/webaudio/AudioWorkletMessagingProxy.cpp
index f206a9022e1e..dd7c154f6d79 100644
--- a/Source/WebCore/Modules/webaudio/AudioWorkletMessagingProxy.cpp
+++ b/Source/WebCore/Modules/webaudio/AudioWorkletMessagingProxy.cpp
@@ -60,6 +60,7 @@ static WorkletParameters generateWorkletParameters(AudioWorklet& worklet)
         document->settingsValues(),
         document->referrerPolicy(),
         worklet.audioContext() ? !worklet.audioContext()->isOfflineContext() : false,
+        document->advancedPrivacyProtections(),
         document->noiseInjectionHashSalt()
     };
 }
diff --git a/Source/WebCore/dom/Document.cpp b/Source/WebCore/dom/Document.cpp
index 7cb301c6d086..0384dc973c69 100644
--- a/Source/WebCore/dom/Document.cpp
+++ b/Source/WebCore/dom/Document.cpp
@@ -10463,13 +10463,18 @@ void Document::resetObservationSizeForContainIntrinsicSize(Element& target)
 
 NoiseInjectionPolicy Document::noiseInjectionPolicy() const
 {
-    if (RefPtr loader = topDocument().loader()) {
-        if (loader->advancedPrivacyProtections().contains(AdvancedPrivacyProtections::FingerprintingProtections))
-            return NoiseInjectionPolicy::Minimal;
-    }
+    if (advancedPrivacyProtections().contains(AdvancedPrivacyProtections::FingerprintingProtections))
+        return NoiseInjectionPolicy::Minimal;
     return NoiseInjectionPolicy::None;
 }
 
+OptionSet<AdvancedPrivacyProtections> Document::advancedPrivacyProtections() const
+{
+    if (RefPtr loader = topDocument().loader())
+        return loader->advancedPrivacyProtections();
+    return { };
+}
+
 std::optional<uint64_t> Document::noiseInjectionHashSalt() const
 {
     if (!page() || noiseInjectionPolicy() == NoiseInjectionPolicy::None)
diff --git a/Source/WebCore/dom/Document.h b/Source/WebCore/dom/Document.h
index 6671f1981332..79f0a3bbfabc 100644
--- a/Source/WebCore/dom/Document.h
+++ b/Source/WebCore/dom/Document.h
@@ -1401,6 +1401,8 @@ public:
     WEBCORE_EXPORT void exitPointerLock();
 #endif
 
+    OptionSet<AdvancedPrivacyProtections> advancedPrivacyProtections() const final;
+
     std::optional<uint64_t> noiseInjectionHashSalt() const final;
     NoiseInjectionPolicy noiseInjectionPolicy() const;
 
diff --git a/Source/WebCore/dom/EmptyScriptExecutionContext.h b/Source/WebCore/dom/EmptyScriptExecutionContext.h
index 3d94e806750a..56f312d0f3ec 100644
--- a/Source/WebCore/dom/EmptyScriptExecutionContext.h
+++ b/Source/WebCore/dom/EmptyScriptExecutionContext.h
@@ -25,6 +25,7 @@
 
 #pragma once
 
+#include "AdvancedPrivacyProtections.h"
 #include "EventLoop.h"
 #include "Microtasks.h"
 #include "ReferrerPolicy.h"
@@ -69,6 +70,7 @@ public:
 
     SecurityOrigin& topOrigin() const final { return m_origin.get(); };
 
+    OptionSet<AdvancedPrivacyProtections> advancedPrivacyProtections() const final { return { }; }
     std::optional<uint64_t> noiseInjectionHashSalt() const { return std::nullopt; }
 
     void postTask(Task&&) final { ASSERT_NOT_REACHED(); }
diff --git a/Source/WebCore/dom/ScriptExecutionContext.h b/Source/WebCore/dom/ScriptExecutionContext.h
index 86e4270d8640..b6c00948de5b 100644
--- a/Source/WebCore/dom/ScriptExecutionContext.h
+++ b/Source/WebCore/dom/ScriptExecutionContext.h
@@ -44,6 +44,7 @@
 #include <wtf/HashSet.h>
 #include <wtf/NativePromise.h>
 #include <wtf/ObjectIdentifier.h>
+#include <wtf/OptionSet.h>
 #include <wtf/URL.h>
 #include <wtf/WeakPtr.h>
 #include <wtf/text/WTFString.h>
@@ -84,6 +85,7 @@ class ServiceWorker;
 class ServiceWorkerContainer;
 class SocketProvider;
 class WebCoreOpaqueRoot;
+enum class AdvancedPrivacyProtections : uint16_t;
 enum class LoadedFromOpaqueSource : bool;
 enum class TaskSource : uint8_t;
 
@@ -141,6 +143,7 @@ public:
 
     virtual GraphicsClient* graphicsClient() { return nullptr; }
 
+    virtual OptionSet<AdvancedPrivacyProtections> advancedPrivacyProtections() const = 0;
     virtual std::optional<uint64_t> noiseInjectionHashSalt() const = 0;
 
     virtual RefPtr<RTCDataChannelRemoteHandlerConnection> createRTCDataChannelRemoteHandlerConnection();
diff --git a/Source/WebCore/page/Page.cpp b/Source/WebCore/page/Page.cpp
index 9f9ac6c92ece..e244fa252bfe 100644
--- a/Source/WebCore/page/Page.cpp
+++ b/Source/WebCore/page/Page.cpp
@@ -21,6 +21,7 @@
 #include "Page.h"
 
 #include "ActivityStateChangeObserver.h"
+#include "AdvancedPrivacyProtections.h"
 #include "AlternativeTextClient.h"
 #include "AnimationFrameRate.h"
 #include "AppHighlightStorage.h"
@@ -4546,7 +4547,7 @@ ModelPlayerProvider& Page::modelPlayerProvider()
     return m_modelPlayerProvider.get();
 }
 
-void Page::setupForRemoteWorker(const URL& scriptURL, const SecurityOriginData& topOrigin, const String& referrerPolicy)
+void Page::setupForRemoteWorker(const URL& scriptURL, const SecurityOriginData& topOrigin, const String& referrerPolicy, OptionSet<AdvancedPrivacyProtections> advancedPrivacyProtections)
 {
     RefPtr localMainFrame = dynamicDowncast<LocalFrame>(mainFrame());
     if (!localMainFrame)
@@ -4562,6 +4563,9 @@ void Page::setupForRemoteWorker(const URL& scriptURL, const SecurityOriginData&
     document->setSiteForCookies(originAsURL);
     document->setFirstPartyForCookies(originAsURL);
 
+    if (RefPtr documentLoader = localMainFrame->checkedLoader()->documentLoader())
+        documentLoader->setAdvancedPrivacyProtections(advancedPrivacyProtections);
+
     if (document->settings().storageBlockingPolicy() != StorageBlockingPolicy::BlockThirdParty)
         document->setDomainForCachePartition(String { emptyString() });
     else
diff --git a/Source/WebCore/page/Page.h b/Source/WebCore/page/Page.h
index e84efcfed6bb..06552bf074a1 100644
--- a/Source/WebCore/page/Page.h
+++ b/Source/WebCore/page/Page.h
@@ -187,6 +187,7 @@ using PlatformDisplayID = uint32_t;
 using SharedStringHash = uint32_t;
 
 enum class ActivityState : uint16_t;
+enum class AdvancedPrivacyProtections : uint16_t;
 enum class CanWrap : bool;
 enum class DidWrap : bool;
 enum class DisabledAdaptations : uint8_t;
@@ -301,7 +302,7 @@ public:
     WEBCORE_EXPORT static void updateStyleForAllPagesAfterGlobalChangeInEnvironment();
     WEBCORE_EXPORT static void clearPreviousItemFromAllPages(HistoryItem*);
 
-    WEBCORE_EXPORT void setupForRemoteWorker(const URL& scriptURL, const SecurityOriginData& topOrigin, const String& referrerPolicy);
+    WEBCORE_EXPORT void setupForRemoteWorker(const URL& scriptURL, const SecurityOriginData& topOrigin, const String& referrerPolicy, OptionSet<AdvancedPrivacyProtections>);
 
     WEBCORE_EXPORT void updateStyleAfterChangeInEnvironment();
 
diff --git a/Source/WebCore/workers/Worker.cpp b/Source/WebCore/workers/Worker.cpp
index 7187397691f1..0c9e7e6e5c20 100644
--- a/Source/WebCore/workers/Worker.cpp
+++ b/Source/WebCore/workers/Worker.cpp
@@ -217,6 +217,7 @@ void Worker::notifyFinished()
     WorkerInitializationData initializationData {
         m_scriptLoader->takeServiceWorkerData(),
         m_clientIdentifier,
+        m_scriptLoader->advancedPrivacyProtections(),
         context->userAgent(m_scriptLoader->responseURL())
     };
     m_contextProxy.startWorkerGlobalScope(m_scriptLoader->responseURL(), *sessionID, m_options.name, WTFMove(initializationData), m_scriptLoader->script(), contentSecurityPolicyResponseHeaders, m_shouldBypassMainWorldContentSecurityPolicy, m_scriptLoader->crossOriginEmbedderPolicy(), m_workerCreationTime, referrerPolicy, m_options.type, m_options.credentials, m_runtimeFlags);
diff --git a/Source/WebCore/workers/WorkerGlobalScope.cpp b/Source/WebCore/workers/WorkerGlobalScope.cpp
index 5379af249827..6dd365ff802f 100644
--- a/Source/WebCore/workers/WorkerGlobalScope.cpp
+++ b/Source/WebCore/workers/WorkerGlobalScope.cpp
@@ -100,7 +100,7 @@ static WorkQueue& sharedFileSystemStorageQueue()
 WTF_MAKE_ISO_ALLOCATED_IMPL(WorkerGlobalScope);
 
 WorkerGlobalScope::WorkerGlobalScope(WorkerThreadType type, const WorkerParameters& params, Ref<SecurityOrigin>&& origin, WorkerThread& thread, Ref<SecurityOrigin>&& topOrigin, IDBClient::IDBConnectionProxy* connectionProxy, SocketProvider* socketProvider, std::unique_ptr<WorkerClient>&& workerClient)
-    : WorkerOrWorkletGlobalScope(type, params.sessionID, isMainThread() ? Ref { commonVM() } : JSC::VM::create(), params.referrerPolicy, &thread, params.noiseInjectionHashSalt, params.clientIdentifier)
+    : WorkerOrWorkletGlobalScope(type, params.sessionID, isMainThread() ? Ref { commonVM() } : JSC::VM::create(), params.referrerPolicy, &thread, params.noiseInjectionHashSalt, params.advancedPrivacyProtections, params.clientIdentifier)
     , m_url(params.scriptURL)
     , m_ownerURL(params.ownerURL)
     , m_inspectorIdentifier(params.inspectorIdentifier)
diff --git a/Source/WebCore/workers/WorkerInitializationData.h b/Source/WebCore/workers/WorkerInitializationData.h
index f2a73bd68487..eab8d841996d 100644
--- a/Source/WebCore/workers/WorkerInitializationData.h
+++ b/Source/WebCore/workers/WorkerInitializationData.h
@@ -25,9 +25,11 @@
 
 #pragma once
 
+#include "AdvancedPrivacyProtections.h"
 #include "ScriptExecutionContextIdentifier.h"
 #include "ServiceWorkerData.h"
 #include <wtf/CrossThreadCopier.h>
+#include <wtf/OptionSet.h>
 #include <wtf/text/WTFString.h>
 
 namespace WebCore {
@@ -35,6 +37,7 @@ namespace WebCore {
 struct WorkerInitializationData {
     std::optional<ServiceWorkerData> serviceWorkerData;
     std::optional<ScriptExecutionContextIdentifier> clientIdentifier;
+    OptionSet<WebCore::AdvancedPrivacyProtections> advancedPrivacyProtections;
     String userAgent;
 
     WorkerInitializationData isolatedCopy() const;
@@ -45,6 +48,7 @@ inline WorkerInitializationData WorkerInitializationData::isolatedCopy() const
     return {
         crossThreadCopy(serviceWorkerData),
         clientIdentifier,
+        advancedPrivacyProtections,
         userAgent.isolatedCopy()
     };
 }
diff --git a/Source/WebCore/workers/WorkerMessagingProxy.cpp b/Source/WebCore/workers/WorkerMessagingProxy.cpp
index 7cc4c7b8a2dd..65277b28721b 100644
--- a/Source/WebCore/workers/WorkerMessagingProxy.cpp
+++ b/Source/WebCore/workers/WorkerMessagingProxy.cpp
@@ -149,6 +149,7 @@ void WorkerMessagingProxy::startWorkerGlobalScope(const URL& scriptURL, PAL::Ses
     WorkerParameters params { scriptURL, m_scriptExecutionContext->url(), name, identifier, WTFMove(initializationData.userAgent), isOnline, contentSecurityPolicyResponseHeaders, shouldBypassMainWorldContentSecurityPolicy, crossOriginEmbedderPolicy, timeOrigin, referrerPolicy, workerType, credentials, m_scriptExecutionContext->settingsValues(), WorkerThreadMode::CreateNewThread, sessionID,
         WTFMove(initializationData.serviceWorkerData),
         initializationData.clientIdentifier.value_or(ScriptExecutionContextIdentifier { }),
+        m_scriptExecutionContext->advancedPrivacyProtections(),
         m_scriptExecutionContext->noiseInjectionHashSalt()
     };
     auto thread = DedicatedWorkerThread::create(params, sourceCode, *this, *this, *this, *this, startMode, m_scriptExecutionContext->topOrigin(), proxy, socketProvider, runtimeFlags);
diff --git a/Source/WebCore/workers/WorkerOrWorkletGlobalScope.cpp b/Source/WebCore/workers/WorkerOrWorkletGlobalScope.cpp
index 45feab347dca..97be22eda704 100644
--- a/Source/WebCore/workers/WorkerOrWorkletGlobalScope.cpp
+++ b/Source/WebCore/workers/WorkerOrWorkletGlobalScope.cpp
@@ -40,7 +40,7 @@ namespace WebCore {
 
 WTF_MAKE_ISO_ALLOCATED_IMPL(WorkerOrWorkletGlobalScope);
 
-WorkerOrWorkletGlobalScope::WorkerOrWorkletGlobalScope(WorkerThreadType type, PAL::SessionID sessionID, Ref<JSC::VM>&& vm, ReferrerPolicy referrerPolicy, WorkerOrWorkletThread* thread, std::optional<uint64_t> noiseInjectionHashSalt, ScriptExecutionContextIdentifier contextIdentifier)
+WorkerOrWorkletGlobalScope::WorkerOrWorkletGlobalScope(WorkerThreadType type, PAL::SessionID sessionID, Ref<JSC::VM>&& vm, ReferrerPolicy referrerPolicy, WorkerOrWorkletThread* thread, std::optional<uint64_t> noiseInjectionHashSalt, OptionSet<AdvancedPrivacyProtections> advancedPrivacyProtections, ScriptExecutionContextIdentifier contextIdentifier)
     : ScriptExecutionContext(Type::WorkerOrWorkletGlobalScope, contextIdentifier)
     , m_script(makeUnique<WorkerOrWorkletScriptController>(type, WTFMove(vm), this))
     , m_moduleLoader(makeUnique<ScriptModuleLoader>(this, ScriptModuleLoader::OwnerType::WorkerOrWorklet))
@@ -49,6 +49,7 @@ WorkerOrWorkletGlobalScope::WorkerOrWorkletGlobalScope(WorkerThreadType type, PA
     , m_sessionID(sessionID)
     , m_referrerPolicy(referrerPolicy)
     , m_noiseInjectionHashSalt(noiseInjectionHashSalt)
+    , m_advancedPrivacyProtections(advancedPrivacyProtections)
 {
     relaxAdoptionRequirement();
 }
diff --git a/Source/WebCore/workers/WorkerOrWorkletGlobalScope.h b/Source/WebCore/workers/WorkerOrWorkletGlobalScope.h
index fc64ee1a76e0..9dd71ea166f6 100644
--- a/Source/WebCore/workers/WorkerOrWorkletGlobalScope.h
+++ b/Source/WebCore/workers/WorkerOrWorkletGlobalScope.h
@@ -39,6 +39,8 @@ class WorkerInspectorController;
 class WorkerOrWorkletScriptController;
 class WorkerOrWorkletThread;
 
+enum class AdvancedPrivacyProtections : uint16_t;
+
 class WorkerOrWorkletGlobalScope : public RefCounted<WorkerOrWorkletGlobalScope>, public ScriptExecutionContext, public EventTarget {
     WTF_MAKE_ISO_ALLOCATED(WorkerOrWorkletGlobalScope);
     WTF_MAKE_NONCOPYABLE(WorkerOrWorkletGlobalScope);
@@ -83,9 +85,10 @@ public:
     virtual FetchOptions::Destination destination() const = 0;
     ReferrerPolicy referrerPolicy() const final { return m_referrerPolicy; }
     std::optional<uint64_t> noiseInjectionHashSalt() const final { return m_noiseInjectionHashSalt; }
+    OptionSet<AdvancedPrivacyProtections> advancedPrivacyProtections() const final { return m_advancedPrivacyProtections; }
 
 protected:
-    WorkerOrWorkletGlobalScope(WorkerThreadType, PAL::SessionID, Ref<JSC::VM>&&, ReferrerPolicy, WorkerOrWorkletThread*, std::optional<uint64_t>,  ScriptExecutionContextIdentifier = { });
+    WorkerOrWorkletGlobalScope(WorkerThreadType, PAL::SessionID, Ref<JSC::VM>&&, ReferrerPolicy, WorkerOrWorkletThread*, std::optional<uint64_t>, OptionSet<AdvancedPrivacyProtections>, ScriptExecutionContextIdentifier = { });
 
     // ScriptExecutionContext.
     bool isJSExecutionForbidden() const final;
@@ -116,6 +119,7 @@ private:
     ReferrerPolicy m_referrerPolicy;
     bool m_isClosing { false };
     std::optional<uint64_t> m_noiseInjectionHashSalt;
+    OptionSet<AdvancedPrivacyProtections> m_advancedPrivacyProtections;
 };
 
 } // namespace WebCore
diff --git a/Source/WebCore/workers/WorkerScriptLoader.cpp b/Source/WebCore/workers/WorkerScriptLoader.cpp
index 0cddb09ca9e3..c3ce081c5981 100644
--- a/Source/WebCore/workers/WorkerScriptLoader.cpp
+++ b/Source/WebCore/workers/WorkerScriptLoader.cpp
@@ -76,6 +76,7 @@ std::optional<Exception> WorkerScriptLoader::loadSynchronously(ScriptExecutionCo
     m_source = source;
     m_destination = FetchOptions::Destination::Script;
     m_isCOEPEnabled = scriptExecutionContext->settingsValues().crossOriginEmbedderPolicyEnabled;
+    m_advancedPrivacyProtections = scriptExecutionContext->advancedPrivacyProtections();
 
     auto* serviceWorkerGlobalScope = dynamicDowncast<ServiceWorkerGlobalScope>(workerGlobalScope);
     if (serviceWorkerGlobalScope) {
@@ -131,6 +132,7 @@ void WorkerScriptLoader::loadAsynchronously(ScriptExecutionContext& scriptExecut
     m_destination = fetchOptions.destination;
     m_isCOEPEnabled = scriptExecutionContext.settingsValues().crossOriginEmbedderPolicyEnabled;
     m_clientIdentifier = clientIdentifier;
+    m_advancedPrivacyProtections = scriptExecutionContext.advancedPrivacyProtections();
 
     ASSERT(scriptRequest.httpMethod() == "GET"_s);
 
diff --git a/Source/WebCore/workers/WorkerScriptLoader.h b/Source/WebCore/workers/WorkerScriptLoader.h
index 84cfbb4152d8..313d9ec77a69 100644
--- a/Source/WebCore/workers/WorkerScriptLoader.h
+++ b/Source/WebCore/workers/WorkerScriptLoader.h
@@ -26,6 +26,7 @@
 
 #pragma once
 
+#include "AdvancedPrivacyProtections.h"
 #include "CertificateInfo.h"
 #include "ContentSecurityPolicyResponseHeaders.h"
 #include "CrossOriginEmbedderPolicy.h"
@@ -40,6 +41,7 @@
 #include "ThreadableLoaderClient.h"
 #include <memory>
 #include <wtf/FastMalloc.h>
+#include <wtf/OptionSet.h>
 #include <wtf/RefCounted.h>
 #include <wtf/RefPtr.h>
 #include <wtf/URL.h>
@@ -71,6 +73,8 @@ public:
 
     void notifyError();
 
+    OptionSet<AdvancedPrivacyProtections> advancedPrivacyProtections() const { return m_advancedPrivacyProtections; }
+
     const ScriptBuffer& script() const { return m_script; }
     const ContentSecurityPolicyResponseHeaders& contentSecurityPolicy() const { return m_contentSecurityPolicy; }
     const String& referrerPolicy() const { return m_referrerPolicy; }
@@ -161,6 +165,7 @@ private:
     RefPtr<ServiceWorkerDataManager> m_serviceWorkerDataManager;
     WeakPtr<ScriptExecutionContext> m_context;
     String m_userAgentForSharedWorker;
+    OptionSet<AdvancedPrivacyProtections> m_advancedPrivacyProtections;
 };
 
 } // namespace WebCore
diff --git a/Source/WebCore/workers/WorkerThread.cpp b/Source/WebCore/workers/WorkerThread.cpp
index 80c6b2b61145..cbe7ed0b49b2 100644
--- a/Source/WebCore/workers/WorkerThread.cpp
+++ b/Source/WebCore/workers/WorkerThread.cpp
@@ -27,6 +27,7 @@
 #include "config.h"
 #include "WorkerThread.h"
 
+#include "AdvancedPrivacyProtections.h"
 #include "IDBConnectionProxy.h"
 #include "ScriptSourceCode.h"
 #include "SecurityOrigin.h"
@@ -67,6 +68,7 @@ WorkerParameters WorkerParameters::isolatedCopy() const
         sessionID,
         crossThreadCopy(serviceWorkerData),
         clientIdentifier,
+        advancedPrivacyProtections,
         noiseInjectionHashSalt
     };
 }
diff --git a/Source/WebCore/workers/WorkerThread.h b/Source/WebCore/workers/WorkerThread.h
index 126e83dabb57..aa595f6029a9 100644
--- a/Source/WebCore/workers/WorkerThread.h
+++ b/Source/WebCore/workers/WorkerThread.h
@@ -62,6 +62,8 @@ namespace IDBClient {
 class IDBConnectionProxy;
 }
 
+enum class AdvancedPrivacyProtections : uint16_t;
+
 struct WorkerThreadStartupData;
 
 struct WorkerParameters {
@@ -84,6 +86,7 @@ public:
     PAL::SessionID sessionID;
     std::optional<ServiceWorkerData> serviceWorkerData;
     ScriptExecutionContextIdentifier clientIdentifier;
+    OptionSet<AdvancedPrivacyProtections> advancedPrivacyProtections;
     std::optional<uint64_t> noiseInjectionHashSalt;
 
     WorkerParameters isolatedCopy() const;
diff --git a/Source/WebCore/workers/service/ServiceWorkerClientData.cpp b/Source/WebCore/workers/service/ServiceWorkerClientData.cpp
index ba7a9da670a5..21c7dc8f604f 100644
--- a/Source/WebCore/workers/service/ServiceWorkerClientData.cpp
+++ b/Source/WebCore/workers/service/ServiceWorkerClientData.cpp
@@ -26,6 +26,7 @@
 #include "config.h"
 #include "ServiceWorkerClientData.h"
 
+#include "AdvancedPrivacyProtections.h"
 #include "Document.h"
 #include "DocumentLoader.h"
 #include "FrameDestructionObserverInlines.h"
@@ -59,12 +60,12 @@ static ServiceWorkerClientFrameType toServiceWorkerClientFrameType(ScriptExecuti
 
 ServiceWorkerClientData ServiceWorkerClientData::isolatedCopy() const &
 {
-    return { identifier, type, frameType, url.isolatedCopy(), ownerURL.isolatedCopy(), pageIdentifier, frameIdentifier, lastNavigationWasAppInitiated, isVisible, isFocused, focusOrder, crossThreadCopy(ancestorOrigins) };
+    return { identifier, type, frameType, url.isolatedCopy(), ownerURL.isolatedCopy(), pageIdentifier, frameIdentifier, lastNavigationWasAppInitiated, advancedPrivacyProtections, isVisible, isFocused, focusOrder, crossThreadCopy(ancestorOrigins) };
 }
 
 ServiceWorkerClientData ServiceWorkerClientData::isolatedCopy() &&
 {
-    return { identifier, type, frameType, WTFMove(url).isolatedCopy(), WTFMove(ownerURL).isolatedCopy(), pageIdentifier, frameIdentifier, lastNavigationWasAppInitiated, isVisible, isFocused, focusOrder, crossThreadCopy(WTFMove(ancestorOrigins)) };
+    return { identifier, type, frameType, WTFMove(url).isolatedCopy(), WTFMove(ownerURL).isolatedCopy(), pageIdentifier, frameIdentifier, lastNavigationWasAppInitiated, advancedPrivacyProtections, isVisible, isFocused, focusOrder, crossThreadCopy(WTFMove(ancestorOrigins)) };
 }
 
 ServiceWorkerClientData ServiceWorkerClientData::from(ScriptExecutionContext& context)
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/AdvancedPrivacyProtections.mm b/Tools/TestWebKitAPI/Tests/WebKit/AdvancedPrivacyProtections.mm
index 625ac7438b10..907dd2c05883 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/AdvancedPrivacyProtections.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/AdvancedPrivacyProtections.mm
@@ -1157,6 +1157,96 @@ TEST(AdvancedPrivacyProtections, Canvas2DQuirks)
     EXPECT_TRUE(finishedSuccessfully);
 }
 
+inline static String sharedWorkerMainBytes()
+{
+    return R"TESTRESOURCE(
+        <!DOCTYPE html>
+        <html>
+        <head>
+        <script>
+        const worker = new SharedWorker("SharedWorker.js");
+        worker.port.onmessage = function(event) {
+            document.querySelector("code").textContent = event.data;
+        };
+        worker.port.postMessage({ command: "draw" });
+        </script>
+        </head>
+        <body>
+            <h1>SharedWorker</h1>
+            The result is: <code></code>
+        </body>
+        </html>
+        )TESTRESOURCE"_s;
+}
+
+inline static String sharedWorkerBytes()
+{
+    return R"TESTRESOURCE(
+        onconnect = (event) => {
+            const port = event.ports[0];
+            port.onmessage = function(event) {
+                if (event.data.command !== "draw")
+                    return;
+
+                const offscreen = new OffscreenCanvas(200, 200);
+                const context = offscreen.getContext("2d");
+
+                const gradient = context.createLinearGradient(0, 0, 200, 0);
+                gradient.addColorStop(0, "red");
+                gradient.addColorStop(1, "blue");
+
+                context.fillStyle = gradient;
+                context.fillRect(0, 0, 200, 200);
+
+                context.fillStyle = "white";
+                context.font = "48px serif";
+                context.fillText("🙃", 10, 50);
+
+                offscreen.convertToBlob().then(blob => {
+                    const reader = new FileReader;
+                    reader.onloadend = async () => {
+                        const base64 = reader.result.split(",")[1];
+                        const bytes = new Uint8Array(atob(base64).split("").map(char => char.charCodeAt(0)));
+                        const hashBuffer = await crypto.subtle.digest("SHA-256", bytes);
+                        const hashArray = Array.from(new Uint8Array(hashBuffer));
+                        const hexRepresentation = hashArray.map(byte => byte.toString(16).padStart(2, "0")).join("");
+                        port.postMessage(hexRepresentation);
+                    };
+                    reader.readAsDataURL(blob);
+                });
+            };
+        };
+        )TESTRESOURCE"_s;
+}
+
+TEST(AdvancedPrivacyProtections, NoiseInjectionForOffscreenCanvasInSharedWorker)
+{
+    auto server = HTTPServer { {
+        { "/"_s, { sharedWorkerMainBytes() } },
+        { "/SharedWorker.js"_s, { { { "Content-Type"_s, "text/javascript"_s } }, sharedWorkerBytes() } }
+    } };
+
+    auto computeHash = [&](TestWKWebView *webView) {
+        [webView synchronouslyLoadRequest:server.request("/"_s)];
+
+        RetainPtr<NSString> result;
+        Util::waitForConditionWithLogging([&] {
+            result = [webView stringByEvaluatingJavaScript:@"document.querySelector('code').textContent"];
+            return [result length] > 0;
+        }, 3, @"Failed to compute hash using OffscreenCanvas.");
+        return result.autorelease();
+    };
+
+    RetainPtr hashWithoutNoiseInjectionInEphemeralStore = computeHash(createWebViewWithAdvancedPrivacyProtections(NO, nil, WKWebsiteDataStore.nonPersistentDataStore).get());
+    RetainPtr hashWithoutNoiseInjectionInDefaultStore = computeHash(createWebViewWithAdvancedPrivacyProtections(NO, nil, WKWebsiteDataStore.defaultDataStore).get());
+    RetainPtr hashWithNoiseInjectionInEphemeralStore = computeHash(createWebViewWithAdvancedPrivacyProtections(YES, nil, WKWebsiteDataStore.nonPersistentDataStore).get());
+    EXPECT_NOT_NULL(hashWithoutNoiseInjectionInEphemeralStore);
+    EXPECT_NOT_NULL(hashWithoutNoiseInjectionInDefaultStore);
+    EXPECT_NOT_NULL(hashWithNoiseInjectionInEphemeralStore);
+    EXPECT_FALSE([hashWithNoiseInjectionInEphemeralStore isEqual:hashWithoutNoiseInjectionInDefaultStore.get()]);
+    EXPECT_FALSE([hashWithNoiseInjectionInEphemeralStore isEqual:hashWithoutNoiseInjectionInEphemeralStore.get()]);
+}
+
 } // namespace TestWebKitAPI
 
 #endif // ENABLE(ADVANCED_PRIVACY_PROTECTIONS)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker.