High firefox Sandbox Escape 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionSandbox escape in the DOM: Navigation component
ComponentDOM
Bug ClassSandbox Escape
Tracker2040160
Fix commit54dc16d08771 (firefox) +18/-0
CISA KEVNot listed
CreditedYaqoub Aldurayhim
Disclosed2026-06-16

Background

`srcdoc`
an <iframe srcdoc="..."> document whose HTML is supplied inline and loaded under the special about:srcdoc URI 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 nsIURI is exactly the about:srcdoc URI, the only URI for which inline srcdoc data is legitimate.
`view-source`
a nested-URI scheme (view-source:about:srcdoc) that wraps an inner nsIURI, 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.

Key insight
The single mistake was trusting a content-process-supplied 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

  1. Compromise a content process The attacker already has code execution in a sandboxed renderer and can forge IPC messages to the parent.
  2. Forge a load state They construct an nsDocShellLoadState with mSrcdocData set to attacker HTML but mURI pointing at a chosen non-about:srcdoc URI (or a mismatched view-source: wrapper).
  3. Drive the navigation The unchecked load state reaches nsDocShell, whose else branch wraps the inline data in an input-stream channel for the spoofed URI.
  4. 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

An attacker who has already compromised a content process gains a sandbox escape by loading attacker-controlled markup under an unintended principal or as a top-level document. The precondition is renderer-level code execution capable of sending crafted nsDocShellLoadState IPC; the payoff is escaping the origin/process confinement the srcdoc invariant was meant to enforce.

Changed Functions

FunctionChangeNotes
if
dom/base/Document.cpp
modified

Files Changed

  • docshell/base/nsDocShell.cpp
  • docshell/base/nsDocShellLoadState.cpp
  • dom/base/Document.cpp
  • netwerk/protocol/viewsource/nsViewSourceChannel.cpp

Audit Directions

  • IPC-supplied URI/data invariants
    Audit 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 trust
    Trace 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 promotion
    Review paths where inheriting or inline content documents could become top-level, since a context change can escalate the principal or bypass sandbox confinement.
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
Loading diff…