CVE-2026-74967
Overview
Background
- Same-origin policy
- the browser rule that content from one origin may not read the response body or error detail of a resource from a different origin.
- `HTMLMediaElement`
- the Gecko C++ class backing
<audio>and<video>elements, which loads media resources over the network and reports load errors to script. - `OnChannelRedirect`
- the
nsIChannelEventSinkcallback invoked when a media load’s HTTP channel follows a redirect to a new URI. - `nsIPrincipal::IsSameOrigin`
- a security-check method that compares a principal’s origin against a given
nsIURIto decide whether a load is same-origin.
Root Cause Analysis
When a media load failed, HTMLMediaElement::NoSupportedMediaSourceError decided whether to redact sensitive error details (MIME type, HTTP status) by comparing mSrcAttrTriggeringPrincipal against mLoadingSrc, which holds the original request URI and is never updated when the channel follows a cross-origin redirect. Because the redirect target’s real origin was never captured, an initially same-origin src that redirected to a cross-origin resource was still treated as same-origin, so the sensitive error detail was leaked. The check also failed open: the guard if (NS_SUCCEEDED(rv) && !isSameOriginLoad) only redacted when the origin comparison succeeded and differed, so any error path (rv failing) skipped redaction entirely.
The fix adds mLoadingSrcFinalURI, which starts equal to mLoadingSrc and is reassigned to the post-redirect URI inside OnChannelRedirect, and compares against mLoadingSrcTriggeringPrincipal and this final URI instead. It also inverts the guard to if (NS_FAILED(rv) || !isSameOriginLoad) so a failed comparison or a null captured URI now redacts (fail-closed). Together these restore the invariant that error detail is disclosed only when the resource that was actually fetched is provably same-origin.
mLoadingSrc) with fail-open logic; the fix tracks the true final loaded URI in mLoadingSrcFinalURI across redirects and treats an unknown or failed comparison as cross-origin.Attack Path
- Host a same-origin redirector
The attacker’s page references a media
srcon its own origin that server-side redirects (e.g. 302) to a victim cross-origin URL. - Trigger the media load
A
<video>or<audio>element loads thesrc, and the channel follows the redirect to the cross-origin target whilemLoadingSrcstays pinned to the original same-origin URI. - Force a load error
The cross-origin response is chosen so the media pipeline reports an unsupported/failed load, invoking
NoSupportedMediaSourceError. - Bypass the origin check
The stale
mLoadingSrccompares as same-origin (or the comparison fails and falls through the fail-open guard), so the detailed, non-generic error is delivered to script. - Read cross-origin detail The page observes the leaked MIME type or HTTP status of the cross-origin resource, inferring cross-origin state.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/media/mediaelement/HTMLMediaElement.cpp |
modified |
Files Changed
dom/media/mediaelement/HTMLMediaElement.cppdom/media/mediaelement/HTMLMediaElement.h
Audit Directions
- Redirect-stale origin checksAudit any security or same-origin decision that reads a request URI (
mLoadingSrc-style fields) rather than the post-redirect final URI captured in anOnChannelRedirect/nsIChannelEventSinkcallback. - Fail-open guard logicFlag origin/permission checks shaped like
if (NS_SUCCEEDED(rv) && !isSameOrigin)that skip protection when the comparison itself fails; correct forms should treat error (NS_FAILED(rv)) or unknown state as untrusted. - Lifecycle reset of URI stateVerify that newly added URI/principal tracking fields are cleared in teardown paths like
AbortExistingLoadsso a prior load’s redirected URI cannot be reused for a subsequent load’s checks.
Patch
diff --git a/dom/media/mediaelement/HTMLMediaElement.cpp b/dom/media/mediaelement/HTMLMediaElement.cpp
index 5db7e05ccff..7272c99c7d0 100644
--- a/dom/media/mediaelement/HTMLMediaElement.cpp
+++ b/dom/media/mediaelement/HTMLMediaElement.cpp
@@ -2575,6 +2575,18 @@ nsresult HTMLMediaElement::OnChannelRedirect(nsIChannel* aChannel,
nsIChannel* aNewChannel,
uint32_t aFlags) {
MOZ_ASSERT(mChannelLoader);
+ if (aNewChannel) {
+ nsCOMPtr<nsIURI> oldURI;
+ if (aChannel) {
+ aChannel->GetURI(getter_AddRefs(oldURI));
+ }
+ aNewChannel->GetURI(getter_AddRefs(mLoadingSrcFinalURI));
+ LOG(LogLevel::Debug,
+ ("{} OnChannelRedirect: from {} to {}", fmt::ptr(this),
+ oldURI ? oldURI->GetSpecOrDefault().get() : "null",
+ mLoadingSrcFinalURI ? mLoadingSrcFinalURI->GetSpecOrDefault().get()
+ : "null"));
+ }
return mChannelLoader->Redirect(aChannel, aNewChannel, aFlags);
}
@@ -2617,6 +2629,8 @@ void HTMLMediaElement::AbortExistingLoads() {
mChannelLoader = nullptr;
}
+ mLoadingSrcFinalURI = nullptr;
+
bool fireTimeUpdate = false;
if (mDecoder) {
@@ -2731,12 +2745,12 @@ void HTMLMediaElement::NoSupportedMediaSourceError(
bool isSameOriginLoad = false;
nsresult rv = NS_ERROR_NOT_AVAILABLE;
- if (mSrcAttrTriggeringPrincipal && mLoadingSrc) {
- rv = mSrcAttrTriggeringPrincipal->IsSameOrigin(mLoadingSrc,
- &isSameOriginLoad);
+ if (mLoadingSrcTriggeringPrincipal && mLoadingSrcFinalURI) {
+ rv = mLoadingSrcTriggeringPrincipal->IsSameOrigin(mLoadingSrcFinalURI,
+ &isSameOriginLoad);
}
- if (NS_SUCCEEDED(rv) && !isSameOriginLoad) {
+ if (NS_FAILED(rv) || !isSameOriginLoad) {
// aErrorDetails can include sensitive details like MimeType or HTTP Status
// Code. In case we're loading a 3rd party resource we should not leak this
// and pass a Generic Error Message
@@ -2968,6 +2982,7 @@ void HTMLMediaElement::SelectResource(
} else {
mLoadingSrc = nullptr;
}
+ mLoadingSrcFinalURI = mLoadingSrc;
mLoadingSrcTriggeringPrincipal = mSrcAttrTriggeringPrincipal;
DDLOG(DDLogCategory::Property, "loading_src",
nsCString(NS_ConvertUTF16toUTF8(src)));
@@ -3255,6 +3270,7 @@ void HTMLMediaElement::LoadFromSourceChildren(
RemoveMediaElementFromURITable();
mLoadingSrc = uri;
+ mLoadingSrcFinalURI = mLoadingSrc;
mLoadingSrcTriggeringPrincipal = child->GetSrcTriggeringPrincipal();
DDLOG(DDLogCategory::Property, "loading_src",
nsCString(NS_ConvertUTF16toUTF8(src)));
diff --git a/dom/media/mediaelement/HTMLMediaElement.h b/dom/media/mediaelement/HTMLMediaElement.h
index e0e0d3658af..163b2151b23 100644
--- a/dom/media/mediaelement/HTMLMediaElement.h
+++ b/dom/media/mediaelement/HTMLMediaElement.h
@@ -1646,6 +1646,12 @@ class HTMLMediaElement : public nsGenericHTMLElement,
// redirects etc.
nsCOMPtr<nsIURI> mLoadingSrc;
+ // The URI of the resource actually loaded. Starts equal to mLoadingSrc and
+ // is updated to the post-redirect URI on each redirect. Used to decide
+ // cross-origin load-error redaction; null means we have no captured URI, and
+ // is treated as cross-origin.
+ nsCOMPtr<nsIURI> mLoadingSrcFinalURI;
+
// The triggering principal for the current source.
nsCOMPtr<nsIPrincipal> mLoadingSrcTriggeringPrincipal;