Released March 31, 2025

CVE-2025-30425

CVSS 4.3 severity medium CWE-284 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 be able to track users in Safari private browsing mode

Researchers: an anonymous researcher

Key insight. ClearKey session IDs came from a process-global static counter, so the script-visible sessionId persisted and incremented across instances and contexts; scoping the counter per CDM instance removes the cross-context tracking signal.

Patch Details

diff --git a/LayoutTests/http/tests/media/clearkey/clear-key-session-id-expected.txt b/LayoutTests/http/tests/media/clearkey/clear-key-session-id-expected.txt
new file mode 100644
index 000000000000..bcf6b799c2a9
--- /dev/null
+++ b/LayoutTests/http/tests/media/clearkey/clear-key-session-id-expected.txt
@@ -0,0 +1,11 @@
+Test that ClearKey session identifiers do not persist across MediaKeySystem instances.
+-
+Create the first pair of sessions
+EXPECTED (session1.sessionId == '0') OK
+EXPECTED (session2.sessionId == '1') OK
+-
+Create the second pair of sessions
+EXPECTED (session1.sessionId == '0') OK
+EXPECTED (session2.sessionId == '1') OK
+END OF TEST
+
diff --git a/LayoutTests/http/tests/media/clearkey/clear-key-session-id.html b/LayoutTests/http/tests/media/clearkey/clear-key-session-id.html
new file mode 100644
index 000000000000..46982acafb80
--- /dev/null
+++ b/LayoutTests/http/tests/media/clearkey/clear-key-session-id.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>clear-key-session-id</title>
+    <script src=../../media-resources/video-test.js></script>
+    <script src="support.js"></script>
+    <script>
+
+    var session1;
+    var session2;
+
+    async function runTest() {
+        let createTwoSessions = async () => {
+            const config = [{ "initDataTypes":["webm"], "audioCapabilities":[{"contentType":"audio/mp4"}]}];
+            let mediaKeySystem = await navigator.requestMediaKeySystemAccess("org.w3.clearkey", config);
+            let mediaKeys = await mediaKeySystem.createMediaKeys();
+
+            const e = new Uint8Array([0]);
+
+            session1 = await mediaKeys.createSession();
+            let request = await session1.generateRequest("webm", e);
+
+            testExpected('session1.sessionId', '0');
+
+            session2 = await mediaKeys.createSession();
+            request = await session2.generateRequest("webm", e);
+
+            testExpected('session2.sessionId', '1');
+        };
+
+        consoleWrite('-');
+        consoleWrite('Create the first pair of sessions');
+        await createTwoSessions();
+
+        consoleWrite('-');
+        consoleWrite('Create the second pair of sessions');
+        await createTwoSessions();
+    }
+
+    window.addEventListener('load', event => {
+        runTest().then(endTest).catch(failTest);
+    });
+    </script>
+</head>
+<body>
+    <div>Test that ClearKey session identifiers do not persist across MediaKeySystem instances.</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.cpp b/Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.cpp
index 45892247809f..78f0e8faaca0 100644
--- a/Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.cpp
+++ b/Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.cpp
@@ -448,10 +448,10 @@ RefPtr<CDMInstanceSession> CDMInstanceClearKey::createSession()
 
 void CDMInstanceSessionClearKey::requestLicense(LicenseType, KeyGroupingStrategy, const AtomString& initDataType, Ref<SharedBuffer>&& initData, LicenseCallback&& callback)
 {
-    static uint32_t s_sessionIdValue = 0;
-    ++s_sessionIdValue;
-
-    m_sessionID = String::number(s_sessionIdValue);
+    if (RefPtr parentInstance = protectedParentInstance())
+        m_sessionID = String::number(parentInstance->getNextSessionIdValue());
+    else
+        m_sessionID = emptyString();
 
     if (equalLettersIgnoringASCIICase(initDataType, "cenc"_s))
         initData = extractKeyidsFromCencInitData(initData.get());
diff --git a/Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.h b/Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.h
index c504566e502e..a1824f5fd161 100644
--- a/Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.h
+++ b/Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.h
@@ -96,6 +96,8 @@ public:
     CDMInstanceClearKey();
     virtual ~CDMInstanceClearKey();
 
+    uint32_t getNextSessionIdValue() { return m_nextSessionIdValue++; }
+
     // CDMInstance
     ImplementationType implementationType() const final { return ImplementationType::ClearKey; }
     void initializeWithConfiguration(const CDMKeySystemConfiguration&, AllowDistinctiveIdentifiers, AllowPersistentState, SuccessCallback&&) final;
@@ -103,6 +105,9 @@ public:
     void setStorageDirectory(const String&) final;
     const String& keySystem() const final;
     RefPtr<CDMInstanceSession> createSession() final;
+
+private:
+    uint32_t m_nextSessionIdValue { 0 };
 };
 
 class CDMInstanceSessionClearKey final : public CDMInstanceSessionProxy {

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-30425
Bugzilla 286580
Component WebCore
Bug Class LogicError
Severity medium
Commit df02f84bef7163f3…
Advisory Apple Advisory

Terminology

  • ClearKey / EME — The Encrypted Media Extensions ClearKey CDM; each media key session receives an identifier exposed to script as session.sessionId.
  • Process-global static counter — A single static incremented across all instances/origins, producing a value that persists across contexts.
  • Private-browsing isolation — Contexts that should not share correlatable state; a global counter defeats that isolation.

Root Cause Analysis

This fixes a private-browsing tracking vector: ClearKey EME session identifiers were drawn from a process-global static counter, exposing a persistent, cross-instance value to script. CDMInstanceSessionClearKey::requestLicense assigned each Encrypted Media session an ID from static uint32_t s_sessionIdValue, incremented on every license request across ALL CDM instances and origins. The resulting session.sessionId (readable by the page) is therefore a monotonic, process-wide value that persists across MediaKeys instances and browsing contexts, letting a site correlate or fingerprint a user — including across isolation boundaries such as private browsing — by observing the counter. The fix scopes the counter to the parent CDM instance: CDMInstanceClearKey gains a per-instance member m_nextSessionIdValue and getNextSessionIdValue(), and requestLicense uses protectedParentInstance()->getNextSessionIdValue() (or an empty string if there is no parent), so session IDs restart per instance and no longer leak a global sequence. The restored invariant is that session identifiers are scoped to their CDM instance rather than a global counter. The regression test creates two MediaKeys instances and checks each yields session IDs 0 and 1 (not a continuing global sequence).

Attack Path

1. Create EME sessions

Use ClearKey EME (requestMediaKeySystemAccess org.w3.clearkey) and create media key sessions.

2. Read session.sessionId

Observe the session ID, which pre-patch came from a global static counter.

3. Correlate across contexts

Because the counter is process-global and monotonic, its value persists across MediaKeys instances and browsing contexts.

4. Track the user

Use the persistent, increasing ID to correlate visits / fingerprint the user, including in private browsing.

Impact Assessment

A privacy/tracking issue in the WebContent process with no memory corruption: a global session-ID counter leaked a persistent, cross-instance value usable to track users, including in Safari private browsing. The impact is correlation/deanonymization rather than code execution.

Audit Directions

Global counters exposed to script

Grep the EME/CDM code (and other platform modules) for static counters/IDs whose values are exposed to web content and can persist across instances/origins.

Session/identifier scoping

Audit identifier generation (session IDs, request IDs) to confirm they are scoped to an instance/origin rather than process-global.

Changed Functions

Function File Change Note
CDMInstanceSessionClearKey::requestLicense Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.cpp modified Derives the session ID from the parent instance’s per-instance counter (getNextSessionIdValue) instead of a process-global static, using an empty string when there is no parent.
CDMInstanceClearKey::getNextSessionIdValue / m_nextSessionIdValue Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.h modified Adds a per-instance session-ID counter so IDs are scoped to the CDM instance.

Files Changed

Source Files

  • Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.cpp
  • Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.h

Test Files

  • LayoutTests/http/tests/media/clearkey/clear-key-session-id-expected.txt
  • LayoutTests/http/tests/media/clearkey/clear-key-session-id.html

Regression Test

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

diff --git a/LayoutTests/http/tests/media/clearkey/clear-key-session-id-expected.txt b/LayoutTests/http/tests/media/clearkey/clear-key-session-id-expected.txt
new file mode 100644
index 000000000000..bcf6b799c2a9
--- /dev/null
+++ b/LayoutTests/http/tests/media/clearkey/clear-key-session-id-expected.txt
@@ -0,0 +1,11 @@
+Test that ClearKey session identifiers do not persist across MediaKeySystem instances.
+-
+Create the first pair of sessions
+EXPECTED (session1.sessionId == '0') OK
+EXPECTED (session2.sessionId == '1') OK
+-
+Create the second pair of sessions
+EXPECTED (session1.sessionId == '0') OK
+EXPECTED (session2.sessionId == '1') OK
+END OF TEST
+
diff --git a/LayoutTests/http/tests/media/clearkey/clear-key-session-id.html b/LayoutTests/http/tests/media/clearkey/clear-key-session-id.html
new file mode 100644
index 000000000000..46982acafb80
--- /dev/null
+++ b/LayoutTests/http/tests/media/clearkey/clear-key-session-id.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>clear-key-session-id</title>
+    <script src=../../media-resources/video-test.js></script>
+    <script src="support.js"></script>
+    <script>
+
+    var session1;
+    var session2;
+
+    async function runTest() {
+        let createTwoSessions = async () => {
+            const config = [{ "initDataTypes":["webm"], "audioCapabilities":[{"contentType":"audio/mp4"}]}];
+            let mediaKeySystem = await navigator.requestMediaKeySystemAccess("org.w3.clearkey", config);
+            let mediaKeys = await mediaKeySystem.createMediaKeys();
+
+            const e = new Uint8Array([0]);
+
+            session1 = await mediaKeys.createSession();
+            let request = await session1.generateRequest("webm", e);
+
+            testExpected('session1.sessionId', '0');
+
+            session2 = await mediaKeys.createSession();
+            request = await session2.generateRequest("webm", e);
+
+            testExpected('session2.sessionId', '1');
+        };
+
+        consoleWrite('-');
+        consoleWrite('Create the first pair of sessions');
+        await createTwoSessions();
+
+        consoleWrite('-');
+        consoleWrite('Create the second pair of sessions');
+        await createTwoSessions();
+    }
+
+    window.addEventListener('load', event => {
+        runTest().then(endTest).catch(failTest);
+    });
+    </script>
+</head>
+<body>
+    <div>Test that ClearKey session identifiers do not persist across MediaKeySystem instances.</div>
+</body>
+</html>
\ No newline at end of file