CVE-2025-43480
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.cppSource/WebCore/loader/MediaResourceLoader.h
Test Files
LayoutTests/http/tests/media/resources/hls/.htaccessLayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-expected.txtLayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors-expected.txtLayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors.htmlLayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media.htmlLayoutTests/platform/glib/TestExpectationsLayoutTests/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