CVE-2024-27830
Overview
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.
Attack Path
- Enter Private Browsing The user browses privately, expecting canvas readback to be noised against fingerprinting.
- 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.
- Read back un-noised pixels Because noise injection was not applied in that context, the readback returns the true, un-perturbed pixels.
- Fingerprint the device Derive a stable device/OS fingerprint from the un-noised output, defeating the Private Browsing protection.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ServiceWorkerContainerSource/WebCore/dom/ScriptExecutionContext.h |
modified | |
SocketProviderSource/WebCore/dom/ScriptExecutionContext.h |
modified | |
WebCoreOpaqueRootSource/WebCore/dom/ScriptExecutionContext.h |
modified | |
WorkerOrWorkletScriptControllerSource/WebCore/workers/WorkerOrWorkletGlobalScope.h |
modified | |
WorkerOrWorkletThreadSource/WebCore/workers/WorkerOrWorkletGlobalScope.h |
modified | |
WorkerOrWorkletGlobalScopeSource/WebCore/workers/WorkerOrWorkletGlobalScope.h |
modified | |
ifSource/WebCore/workers/WorkerScriptLoader.cpp |
modified | |
IDBConnectionProxySource/WebCore/workers/WorkerThread.h |
modified |
Files Changed
Source/WebCore/Modules/webaudio/AudioWorkletMessagingProxy.cppSource/WebCore/dom/Document.cppSource/WebCore/dom/Document.hSource/WebCore/dom/EmptyScriptExecutionContext.hSource/WebCore/dom/ScriptExecutionContext.hSource/WebCore/page/Page.cppSource/WebCore/page/Page.hSource/WebCore/workers/Worker.cppSource/WebCore/workers/WorkerGlobalScope.cppSource/WebCore/workers/WorkerInitializationData.hSource/WebCore/workers/WorkerMessagingProxy.cppSource/WebCore/workers/WorkerOrWorkletGlobalScope.cppSource/WebCore/workers/WorkerOrWorkletGlobalScope.hSource/WebCore/workers/WorkerScriptLoader.cppSource/WebCore/workers/WorkerScriptLoader.hSource/WebCore/workers/WorkerThread.cppSource/WebCore/workers/WorkerThread.hSource/WebCore/workers/service/ServiceWorkerClientData.cppSource/WebCore/workers/service/ServiceWorkerClientData.hSource/WebCore/workers/service/context/ServiceWorkerThread.cppSource/WebCore/workers/service/context/ServiceWorkerThread.hSource/WebCore/workers/service/context/ServiceWorkerThreadProxy.cppSource/WebCore/workers/service/server/SWServer.cppSource/WebCore/workers/service/server/SWServer.hSource/WebCore/workers/service/server/SWServerToContextConnection.hSource/WebCore/workers/shared/SharedWorkerScriptLoader.cppSource/WebCore/workers/shared/context/SharedWorkerThreadProxy.cppSource/WebCore/worklets/WorkletGlobalScope.cppSource/WebCore/worklets/WorkletParameters.hSource/WebKit/NetworkProcess/ServiceWorker/WebSWServerConnection.cppSource/WebKit/NetworkProcess/ServiceWorker/WebSWServerToContextConnection.cppSource/WebKit/NetworkProcess/ServiceWorker/WebSWServerToContextConnection.hSource/WebKit/Shared/WebCoreArgumentCoders.serialization.inSource/WebKit/WebProcess/Storage/WebSWContextManagerConnection.cppSource/WebKit/WebProcess/Storage/WebSWContextManagerConnection.hSource/WebKit/WebProcess/Storage/WebSWContextManagerConnection.messages.inSource/WebKit/WebProcess/Storage/WebSharedWorkerContextManagerConnection.cppTools/TestWebKitAPI/Tests/WebKit/AdvancedPrivacyProtections.mm
Audit Directions
- Privacy-flag plumbing across contextsAudit 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 gapsGrep Page::setupForRemoteWorker and remote-worker init for other per-origin privacy/security state that a remote Page may be missing.
Patch
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)
Regression Test / PoC
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)