CVE-2025-30425
Overview
Background
- 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
- Create EME sessions Use ClearKey EME (requestMediaKeySystemAccess org.w3.clearkey) and create media key sessions.
- Read session.sessionId Observe the session ID, which pre-patch came from a global static counter.
- Correlate across contexts Because the counter is process-global and monotonic, its value persists across MediaKeys instances and browsing contexts.
- Track the user Use the persistent, increasing ID to correlate visits / fingerprint the user, including in private browsing.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
CDMInstanceSessionClearKeySource/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.h |
modified |
Files Changed
LayoutTests/http/tests/media/clearkey/clear-key-session-id-expected.txtLayoutTests/http/tests/media/clearkey/clear-key-session-id.htmlSource/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.cppSource/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.h
Audit Directions
- Global counters exposed to scriptGrep 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 scopingAudit identifier generation (session IDs, request IDs) to confirm they are scoped to an instance/origin rather than process-global.
Patch
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 {
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
Regression Test / PoC
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