Released November 3, 2025

CVE-2025-43480

CVSS 8.1 severity high CWE-942 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N

Overview

Impact:

Description: A malicious website may exfiltrate data cross-origin

Researchers: Aleksejs Popovs

Key insight. MediaResourceLoader failed to mark cross-origin, non-CORS media sub-resources as loaded-from-opaque-source, leaking their cross-origin data/timing; forcing those loads opaque (once the main resource has loaded) restores the boundary.

Patch Details

diff --git a/LayoutTests/http/tests/media/resources/hls/.htaccess b/LayoutTests/http/tests/media/resources/hls/.htaccess
new file mode 100644
index 000000000000..391a152f92b6
--- /dev/null
+++ b/LayoutTests/http/tests/media/resources/hls/.htaccess
@@ -0,0 +1,3 @@
+<Files playlist-with-cookie.m3u8>
+Header always set Access-Control-Allow-Origin "http://127.0.0.1:8000"
+</Files>
diff --git a/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-expected.txt b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-expected.txt
new file mode 100644
index 000000000000..af5a27c8e2d9
--- /dev/null
+++ b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-expected.txt
@@ -0,0 +1,3 @@
+http://127.0.0.1:8000/resources/js-test-pre.js
+http://localhost:8000/media/resources/hls/playlist-with-cookie.m3u8
+
diff --git a/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors-expected.txt b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors-expected.txt
new file mode 100644
index 000000000000..4dee4ff8d4d7
--- /dev/null
+++ b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors-expected.txt
@@ -0,0 +1,8 @@
+CONSOLE MESSAGE: Origin http://127.0.0.1:8000 is not allowed by Access-Control-Allow-Origin. Status code: 200
+CONSOLE MESSAGE: Origin http://127.0.0.1:8000 is not allowed by Access-Control-Allow-Origin. Status code: 200
+CONSOLE MESSAGE: Origin http://127.0.0.1:8000 is not allowed by Access-Control-Allow-Origin. Status code: 200
+CONSOLE MESSAGE: Origin http://127.0.0.1:8000 is not allowed by Access-Control-Allow-Origin. Status code: 200
+http://127.0.0.1:8000/resources/js-test-pre.js
+http://localhost:8000/media/resources/hls/playlist-with-cookie.m3u8
+http://localhost:8000/media/resources/hls/sub-playlist-with-cookie.py
+
diff --git a/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors.html b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors.html
new file mode 100644
index 000000000000..b6d971f485b8
--- /dev/null
+++ b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+    if (window.testRunner) {
+        testRunner.dumpAsText()
+        testRunner.waitUntilDone();
+    }
+</script>
+<script src="../../resources/js-test-pre.js"></script>
+</head>
+<body>
+<video src="http://localhost:8000/media/resources/hls/playlist-with-cookie.m3u8" crossorigin>
+<script>
+    var entries = new Set();
+    let gotMetadata = false;
+    const expectedEntries = 3;
+    let runTest = function() {
+        for (let entry of performance.getEntriesByType('resource')) {
+            if (entries.has(entry.name))
+                continue;
+            entries.add(entry.name);
+            if (entries.size <= expectedEntries)
+                debug(entry.name);
+        }
+
+        if (gotMetadata && entries.size >= expectedEntries)
+            testRunner.notifyDone();
+    };
+    let video = document.querySelector("video");
+    setInterval(runTest, 100);
+
+    video.addEventListener("loadedmetadata", (e) => {
+        gotMetadata = true;
+        if (gotMetadata && entries.size >= expectedEntries)
+            testRunner.notifyDone();
+    });
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media.html b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media.html
new file mode 100644
index 000000000000..77e57e930353
--- /dev/null
+++ b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+    if (window.testRunner) {
+        testRunner.dumpAsText()
+        testRunner.waitUntilDone();
+    }
+</script>
+<script src="../../resources/js-test-pre.js"></script>
+</head>
+<body>
+<video src="http://localhost:8000/media/resources/hls/playlist-with-cookie.m3u8">
+<script>
+    var entries = new Set();
+    let gotMetadata = false;
+
+    let video = document.querySelector("video");
+
+    let runTest = function() {
+        for (let entry of performance.getEntriesByType('resource')) {
+            if (entries.has(entry.name))
+                continue;
+            entries.add(entry.name);
+            if (entry.initiatorType == "video" && entry.name != video.src)
+                testFailed("Unexpected entry: " + entry.name);
+            else
+                debug(entry.name);
+        }
+
+        if (gotMetadata && entries.size)
+            testRunner.notifyDone();
+    };
+    setInterval(runTest, 100);
+
+    video.addEventListener("loadedmetadata", (e) => {
+        gotMetadata = true;
+        if (gotMetadata && entries.size)
+            testRunner.notifyDone();
+    });
+</script>
+</body>
+</html>
diff --git a/LayoutTests/platform/glib/TestExpectations b/LayoutTests/platform/glib/TestExpectations
index 9b96f1df14a3..08b1954dec2e 100644
--- a/LayoutTests/platform/glib/TestExpectations
+++ b/LayoutTests/platform/glib/TestExpectations
@@ -2907,6 +2907,8 @@ http/tests/media/modern-media-controls/time-control/1-to-10-hours.html [ Skip ]
 http/tests/media/modern-media-controls/time-control/10-hours-or-more.html [ Skip ]
 http/tests/media/modern-media-controls/time-control/less-than-10-minutes.html [ Skip ]
 http/tests/media/modern-media-controls/time-control/10-minutes-to-1-hour.html [ Skip ]
+http/tests/performance/performance-resource-timing-cross-origin-media.html [ Skip ]
+http/tests/performance/performance-resource-timing-cross-origin-media-with-cors.html [ Skip ]
 
 # DASH media playback support is disabled in the GStreamer ports.
 platform/glib/media/media-can-play-dash.html [ Skip ]
diff --git a/LayoutTests/platform/win/TestExpectations b/LayoutTests/platform/win/TestExpectations
index 91716eac61a9..680d1957c1b9 100644
--- a/LayoutTests/platform/win/TestExpectations
+++ b/LayoutTests/platform/win/TestExpectations
@@ -859,6 +859,9 @@ webkit.org/b/255747 http/tests/security/canvas-remote-read-remote-video-redirect
 
 http/tests/security/canvas-remote-read-remote-video-hls.html [ Skip ] # Timeout
 
+http/tests/performance/performance-resource-timing-cross-origin-media.html [ Skip ] # Timeout
+http/tests/performance/performance-resource-timing-cross-origin-media-with-cors.html [ Skip ] # Timeout
+
 http/tests/security/clipboard/copy-paste-html-across-origin-sanitizes-html.html [ Skip ] # Failure
 http/tests/security/clipboard/copy-paste-html-across-origin-strips-mso-list.html [ Skip ] # Failure
 http/tests/security/clipboard/copy-paste-html-cross-origin-iframe-across-origin.html [ Skip ] # Failure
diff --git a/Source/WebCore/loader/MediaResourceLoader.cpp b/Source/WebCore/loader/MediaResourceLoader.cpp
index 10de567ac46b..efcd95c91435 100644
--- a/Source/WebCore/loader/MediaResourceLoader.cpp
+++ b/Source/WebCore/loader/MediaResourceLoader.cpp
@@ -123,6 +123,10 @@ RefPtr<PlatformMediaResource> MediaResourceLoader::requestResource(ResourceReque
         cachingPolicy };
     loaderOptions.sameOriginDataURLFlag = SameOriginDataURLFlag::Set;
     loaderOptions.destination = m_destination;
+    if (!m_document->securityOrigin().isSameOriginAs(SecurityOrigin::create(request.url())) && m_crossOriginMode.isNull() && m_wasMainResourceLoaded)
+        loaderOptions.loadedFromOpaqueSource = LoadedFromOpaqueSource::Yes;
+    else if (!m_wasMainResourceLoaded)
+        m_wasMainResourceLoaded = true;
     auto cachedRequest = createPotentialAccessControlRequest(WTFMove(request), WTFMove(loaderOptions), *m_document, m_crossOriginMode);
     if (RefPtr element = m_element.get())
         cachedRequest.setInitiator(*element);
diff --git a/Source/WebCore/loader/MediaResourceLoader.h b/Source/WebCore/loader/MediaResourceLoader.h
index e8c7e08f80ef..56697282020b 100644
--- a/Source/WebCore/loader/MediaResourceLoader.h
+++ b/Source/WebCore/loader/MediaResourceLoader.h
@@ -67,6 +67,7 @@ public:
 private:
     void contextDestroyed() override;
 
+    bool m_wasMainResourceLoaded { false };
     WeakPtr<Document, WeakPtrImplWithEventTargetData> m_document WTF_GUARDED_BY_CAPABILITY(mainThread);
     WeakPtr<Element, WeakPtrImplWithEventTargetData> m_element WTF_GUARDED_BY_CAPABILITY(mainThread);
     String m_crossOriginMode WTF_GUARDED_BY_CAPABILITY(mainThread);

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-43480
Bugzilla 276208
Component WebCore
Bug Class CrossOrigin
Severity medium
Commit 0473037b55025aeb…
Advisory Apple Advisory

Terminology

  • Opaque source (loadedFromOpaqueSource) — Marks a cross-origin response so its contents and detailed timing are hidden from script, per the same-origin policy.
  • MediaResourceLoader — Loads media resources for HTMLMediaElement, including HLS playlists and their sub-resources.
  • CORS mode (m_crossOriginMode) — Whether the media element requested cross-origin access with CORS; null means no CORS, so cross-origin data must stay opaque.
  • Resource Timing — A performance API that can expose per-resource load timing, which must be coarsened/withheld for opaque cross-origin resources.

Root Cause Analysis

MediaResourceLoader loads media resources (e.g. HLS playlists and their segment/sub-resources) for a <video>/<audio> element. Whether a response is treated as an opaque cross-origin resource – which prevents script from observing its data and detailed timing – is controlled by loaderOptions.loadedFromOpaqueSource. Pre-patch, cross-origin media sub-resources fetched after the main resource were not consistently marked opaque, so a cross-origin media stream loaded without CORS could leak data/timing (for example via Resource Timing entries or otherwise observable response state), i.e. cross-origin information disclosure. The fix adds tracking of whether the main resource has loaded (m_wasMainResourceLoaded) and, in requestResource, marks a request opaque when it is cross-origin and uncredentialed by CORS: if (!m_document->securityOrigin().isSameOriginAs(SecurityOrigin::create(request.url())) && m_crossOriginMode.isNull() && m_wasMainResourceLoaded) loaderOptions.loadedFromOpaqueSource = Yes; else if (!m_wasMainResourceLoaded) m_wasMainResourceLoaded = true;. In other words, the first (main) resource load flips the flag, and subsequent cross-origin, non-CORS sub-resource loads are forced to opaque so their contents/timing are not exposed. The restored invariant is that cross-origin media sub-resources without CORS are treated as opaque, closing the cross-origin data/timing leak. The added tests cover cross-origin media resource-timing with and without CORS.

Attack Path

1. Point media at a cross-origin stream

Load a <video>/<audio> whose src is a cross-origin HLS playlist (or similar) that references sub-resources, without proper CORS.

2. Let sub-resources load

After the main playlist loads, the media engine fetches cross-origin segments/sub-resources through MediaResourceLoader.

3. Observe non-opaque responses

Pre-patch, those cross-origin sub-resource loads were not marked loadedFromOpaqueSource, so their data/timing were observable (e.g. Resource Timing).

4. Exfiltrate cross-origin info

Read the leaked cross-origin response data/timing – a same-origin-policy / information-disclosure bypass in WebContent.

Impact Assessment

A cross-origin information-disclosure bug: cross-origin, non-CORS media sub-resources were not marked opaque, exposing their data/timing to script. It is a same-origin-policy leak with no memory-safety component, in the WebContent process. Rated medium.

Audit Directions

Other loadedFromOpaqueSource gaps

grep WebCore/loader and media for loadedFromOpaqueSource and confirm every cross-origin non-CORS load path sets it.

Media sub-resource origins

Audit HLS/DASH/playlist sub-resource fetching for consistent same-origin checks against the document origin, not just the manifest origin.

Timing exposure

Review Resource Timing / server-timing exposure for media resources to ensure opaque responses are coarsened.

Changed Functions

Function File Change Note
MediaResourceLoader::requestResource Source/WebCore/loader/MediaResourceLoader.cpp modified Marks a request loadedFromOpaqueSource when it is cross-origin, CORS mode is null, and the main resource already loaded; the first load sets m_wasMainResourceLoaded.
MediaResourceLoader::m_wasMainResourceLoaded Source/WebCore/loader/MediaResourceLoader.h modified New flag distinguishing the initial main resource load from subsequent (sub-)resource loads.

Files Changed

Source Files

  • Source/WebCore/loader/MediaResourceLoader.cpp
  • Source/WebCore/loader/MediaResourceLoader.h

Test Files

  • LayoutTests/http/tests/media/resources/hls/.htaccess
  • LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-expected.txt
  • LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors-expected.txt
  • LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors.html
  • LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media.html
  • LayoutTests/platform/glib/TestExpectations
  • LayoutTests/platform/win/TestExpectations

Regression Test

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

diff --git a/LayoutTests/http/tests/media/resources/hls/.htaccess b/LayoutTests/http/tests/media/resources/hls/.htaccess
new file mode 100644
index 000000000000..391a152f92b6
--- /dev/null
+++ b/LayoutTests/http/tests/media/resources/hls/.htaccess
@@ -0,0 +1,3 @@
+<Files playlist-with-cookie.m3u8>
+Header always set Access-Control-Allow-Origin "http://127.0.0.1:8000"
+</Files>
diff --git a/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-expected.txt b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-expected.txt
new file mode 100644
index 000000000000..af5a27c8e2d9
--- /dev/null
+++ b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-expected.txt
@@ -0,0 +1,3 @@
+http://127.0.0.1:8000/resources/js-test-pre.js
+http://localhost:8000/media/resources/hls/playlist-with-cookie.m3u8
+
diff --git a/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors-expected.txt b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors-expected.txt
new file mode 100644
index 000000000000..4dee4ff8d4d7
--- /dev/null
+++ b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors-expected.txt
@@ -0,0 +1,8 @@
+CONSOLE MESSAGE: Origin http://127.0.0.1:8000 is not allowed by Access-Control-Allow-Origin. Status code: 200
+CONSOLE MESSAGE: Origin http://127.0.0.1:8000 is not allowed by Access-Control-Allow-Origin. Status code: 200
+CONSOLE MESSAGE: Origin http://127.0.0.1:8000 is not allowed by Access-Control-Allow-Origin. Status code: 200
+CONSOLE MESSAGE: Origin http://127.0.0.1:8000 is not allowed by Access-Control-Allow-Origin. Status code: 200
+http://127.0.0.1:8000/resources/js-test-pre.js
+http://localhost:8000/media/resources/hls/playlist-with-cookie.m3u8
+http://localhost:8000/media/resources/hls/sub-playlist-with-cookie.py
+
diff --git a/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors.html b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors.html
new file mode 100644
index 000000000000..b6d971f485b8
--- /dev/null
+++ b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+    if (window.testRunner) {
+        testRunner.dumpAsText()
+        testRunner.waitUntilDone();
+    }
+</script>
+<script src="../../resources/js-test-pre.js"></script>
+</head>
+<body>
+<video src="http://localhost:8000/media/resources/hls/playlist-with-cookie.m3u8" crossorigin>
+<script>
+    var entries = new Set();
+    let gotMetadata = false;
+    const expectedEntries = 3;
+    let runTest = function() {
+        for (let entry of performance.getEntriesByType('resource')) {
+            if (entries.has(entry.name))
+                continue;
+            entries.add(entry.name);
+            if (entries.size <= expectedEntries)
+                debug(entry.name);
+        }
+
+        if (gotMetadata && entries.size >= expectedEntries)
+            testRunner.notifyDone();
+    };
+    let video = document.querySelector("video");
+    setInterval(runTest, 100);
+
+    video.addEventListener("loadedmetadata", (e) => {
+        gotMetadata = true;
+        if (gotMetadata && entries.size >= expectedEntries)
+            testRunner.notifyDone();
+    });
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media.html b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media.html
new file mode 100644
index 000000000000..77e57e930353
--- /dev/null
+++ b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+    if (window.testRunner) {
+        testRunner.dumpAsText()
+        testRunner.waitUntilDone();
+    }
+</script>
+<script src="../../resources/js-test-pre.js"></script>
+</head>
+<body>
+<video src="http://localhost:8000/media/resources/hls/playlist-with-cookie.m3u8">
+<script>
+    var entries = new Set();
+    let gotMetadata = false;
+
+    let video = document.querySelector("video");
+
+    let runTest = function() {
+        for (let entry of performance.getEntriesByType('resource')) {
+            if (entries.has(entry.name))
+                continue;
+            entries.add(entry.name);
+            if (entry.initiatorType == "video" && entry.name != video.src)
+                testFailed("Unexpected entry: " + entry.name);
+            else
+                debug(entry.name);
+        }
+
+        if (gotMetadata && entries.size)
+            testRunner.notifyDone();
+    };
+    setInterval(runTest, 100);
+
+    video.addEventListener("loadedmetadata", (e) => {
+        gotMetadata = true;
+        if (gotMetadata && entries.size)
+            testRunner.notifyDone();
+    });
+</script>
+</body>
+</html>
diff --git a/LayoutTests/platform/glib/TestExpectations b/LayoutTests/platform/glib/TestExpectations
index 9b96f1df14a3..08b1954dec2e 100644
--- a/LayoutTests/platform/glib/TestExpectations
+++ b/LayoutTests/platform/glib/TestExpectations
@@ -2907,6 +2907,8 @@ http/tests/media/modern-media-controls/time-control/1-to-10-hours.html [ Skip ]
 http/tests/media/modern-media-controls/time-control/10-hours-or-more.html [ Skip ]
 http/tests/media/modern-media-controls/time-control/less-than-10-minutes.html [ Skip ]
 http/tests/media/modern-media-controls/time-control/10-minutes-to-1-hour.html [ Skip ]
+http/tests/performance/performance-resource-timing-cross-origin-media.html [ Skip ]
+http/tests/performance/performance-resource-timing-cross-origin-media-with-cors.html [ Skip ]
 
 # DASH media playback support is disabled in the GStreamer ports.
 platform/glib/media/media-can-play-dash.html [ Skip ]
diff --git a/LayoutTests/platform/win/TestExpectations b/LayoutTests/platform/win/TestExpectations
index 91716eac61a9..680d1957c1b9 100644
--- a/LayoutTests/platform/win/TestExpectations
+++ b/LayoutTests/platform/win/TestExpectations
@@ -859,6 +859,9 @@ webkit.org/b/255747 http/tests/security/canvas-remote-read-remote-video-redirect
 
 http/tests/security/canvas-remote-read-remote-video-hls.html [ Skip ] # Timeout
 
+http/tests/performance/performance-resource-timing-cross-origin-media.html [ Skip ] # Timeout
+http/tests/performance/performance-resource-timing-cross-origin-media-with-cors.html [ Skip ] # Timeout
+
 http/tests/security/clipboard/copy-paste-html-across-origin-sanitizes-html.html [ Skip ] # Failure
 http/tests/security/clipboard/copy-paste-html-across-origin-strips-mso-list.html [ Skip ] # Failure
 http/tests/security/clipboard/copy-paste-html-cross-origin-iframe-across-origin.html [ Skip ] # Failure