CVE-2026-12295
Overview
Background
- `srcdoc`
- an
<iframe srcdoc="...">document whose HTML is supplied inline and loaded under the specialabout:srcdocURI while inheriting its parent’s origin. - `nsDocShellLoadState`
- the serializable object describing a navigation (target URI, load flags, and inline
mSrcdocData) that a content process sends to drive a load. - `NS_IsAboutSrcdoc`
- a helper that returns whether a
nsIURIis exactly theabout:srcdocURI, the only URI for which inline srcdoc data is legitimate. - `view-source`
- a nested-URI scheme (
view-source:about:srcdoc) that wraps an innernsIURI, letting source viewing reuse the srcdoc pipeline.
Root Cause Analysis
A load could carry inline srcdoc content (mSrcdocData) while its mURI was an arbitrary attacker-chosen URI rather than about:srcdoc, because the IPC deserialization of nsDocShellLoadState never enforced the invariant that srcdoc data may only accompany about:srcdoc (or a view-source: wrapper thereof). When such a crafted state reached the channel-creation path in nsDocShell.cpp, the else branch would build an NS_NewInputStreamChannelInternal channel feeding attacker HTML for a mismatched URI, allowing srcdoc content to be treated as a top-level document under an unintended principal.
The fix re-asserts the invariant at every choke point: it rejects malformed states during IPC deserialization via FatalError, adds MOZ_RELEASE_ASSERT(NS_IsAboutSrcdoc(aURI)) before creating the srcdoc input-stream channel, forbids a srcdoc channel from becoming a top-level content document in Document::StartDocumentLoad, and verifies the inner URI of a view-source: srcdoc load is genuinely about:srcdoc. Together these restore the guarantee that inline srcdoc data can only ever be parsed as an about:srcdoc document, closing the confusion that let a compromised content process load its own markup with escalated context.
nsDocShellLoadState that paired inline mSrcdocData with a non-about:srcdoc URI; the fix hardens the trust boundary by validating that pairing at IPC receipt and asserting it again at each place the srcdoc data is consumed.Attack Path
- Compromise a content process The attacker already has code execution in a sandboxed renderer and can forge IPC messages to the parent.
- Forge a load state
They construct an
nsDocShellLoadStatewithmSrcdocDataset to attacker HTML butmURIpointing at a chosen non-about:srcdocURI (or a mismatchedview-source:wrapper). - Drive the navigation
The unchecked load state reaches
nsDocShell, whoseelsebranch wraps the inline data in an input-stream channel for the spoofed URI. - Confuse the document context
The srcdoc content is parsed as a document (potentially top-level) under a principal/context that does not match
about:srcdoc, breaking origin/sandbox expectations.
Impact Assessment
nsDocShellLoadState IPC; the payoff is escaping the origin/process confinement the srcdoc invariant was meant to enforce.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/base/Document.cpp |
modified |
Files Changed
docshell/base/nsDocShell.cppdocshell/base/nsDocShellLoadState.cppdom/base/Document.cppnetwerk/protocol/viewsource/nsViewSourceChannel.cpp
Audit Directions
- IPC-supplied URI/data invariantsAudit every field pair received from a content process (like
mSrcdocData+mURI) where one value implies a constraint on another, and enforce it at deserialization rather than at use. - Special-URI trustTrace all consumers of
about:srcdoc,view-source:, and other privileged or nested URIs to ensure each verifies the actual URI (and inner URI) instead of assuming the pipeline was entered legitimately. - Top-level vs. subframe promotionReview paths where inheriting or inline content documents could become top-level, since a context change can escalate the principal or bypass sandbox confinement.
Patch
diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp
index a2e8d939f8f..92ca9d5eb1d 100644
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -9028,6 +9028,8 @@ nsIPrincipal* nsDocShell::GetInheritedPrincipal(
MOZ_TRY(vsh->NewSrcdocChannel(aURI, aBaseURI, aSrcdoc, aLoadInfo,
getter_AddRefs(channel)));
} else {
+ MOZ_RELEASE_ASSERT(NS_IsAboutSrcdoc(aURI));
+
MOZ_TRY(NS_NewInputStreamChannelInternal(getter_AddRefs(channel), aURI,
aSrcdoc, "text/html"_ns, aLoadInfo,
true));
diff --git a/docshell/base/nsDocShellLoadState.cpp b/docshell/base/nsDocShellLoadState.cpp
index 52e6c0bc633..3d250d97275 100644
--- a/docshell/base/nsDocShellLoadState.cpp
+++ b/docshell/base/nsDocShellLoadState.cpp
@@ -163,6 +163,12 @@ nsDocShellLoadState::nsDocShellLoadState(
}
}
+ if (!mSrcdocData.IsVoid() && !mURI->SchemeIs("view-source") &&
+ !NS_IsAboutSrcdoc(mURI)) {
+ aActor->FatalError("nsDocShellLoadState with invalid srcdoc state");
+ return;
+ }
+
// We successfully read in the data - return a success value.
*aReadSuccess = true;
}
diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp
index d535a519b53..d9a2ce48b01 100644
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -3522,6 +3522,7 @@ nsresult Document::StartDocumentLoad(const char* aCommand, nsIChannel* aChannel,
bool isSrcdocChannel;
inStrmChan->GetIsSrcdocChannel(&isSrcdocChannel);
if (isSrcdocChannel) {
+ MOZ_RELEASE_ASSERT(!IsTopLevelContentDocument());
mIsSrcdocDocument = true;
}
}
diff --git a/netwerk/protocol/viewsource/nsViewSourceChannel.cpp b/netwerk/protocol/viewsource/nsViewSourceChannel.cpp
index 55710b51e14..a4f4841725c 100644
--- a/netwerk/protocol/viewsource/nsViewSourceChannel.cpp
+++ b/netwerk/protocol/viewsource/nsViewSourceChannel.cpp
@@ -11,6 +11,7 @@
#include "nsIHttpHeaderVisitor.h"
#include "nsIIOService.h"
#include "nsIInputStreamChannel.h"
+#include "nsINestedURI.h"
#include "nsIReferrerInfo.h"
#include "nsMimeTypes.h"
#include "nsNetUtil.h"
@@ -108,6 +109,14 @@ nsresult nsViewSourceChannel::InitSrcdoc(nsIURI* aURI, nsIURI* aBaseURI,
nsILoadInfo* aLoadInfo) {
nsresult rv;
+ MOZ_ASSERT(aURI->SchemeIs("view-source"));
+ nsCOMPtr<nsINestedURI> nestedURI(do_QueryInterface(aURI));
+ NS_ENSURE_TRUE(nestedURI, NS_ERROR_INVALID_ARG);
+ nsCOMPtr<nsIURI> innerURI;
+ rv = nestedURI->GetInnerURI(getter_AddRefs(innerURI));
+ NS_ENSURE_SUCCESS(rv, rv);
+ MOZ_RELEASE_ASSERT(NS_IsAboutSrcdoc(innerURI));
+
nsCOMPtr<nsIURI> inStreamURI;
// Need to strip view-source: from the URI. Hardcoded to
// about:srcdoc as this is the only permissible URI for srcdoc