Missing sender check in RecvReloadWithHttpsOnlyException lets a compromised cross-origin iframe downgrade its embedder top-level page from HTTPS to HTTP (persistent, defeats HTTPS-First)
Categories
(Core :: DOM: Security, defect)
Tracking
()
People
(Reporter: part23, Unassigned)
Details
(Keywords: reporter-external, Whiteboard: [client-bounty-form])
Attachments
(1 file)
|
6.43 KB,
application/x-gzip
|
Details |
Description
A compromised content process that renders a cross-origin iframe can force the first-party top-level page that embeds it to reload over HTTP and persist a session "allow insecure" exemption, defeating HTTPS-First. WindowGlobalParent::RecvReloadWithHttpsOnlyException acts on BrowsingContext()->Top() without verifying that the sending WindowGlobalChild is that top (or is the about:httpsonlyerror page it is meant to back), so any descendant frame can trigger the top-level HTTPS→HTTP downgrade.
Product / Component
Core :: DOM: Security (HTTPS-Only Mode) / Process isolation. Desktop, Fission (default).
Affected versions
- mozilla-central / Firefox Nightly 157.0a1 — confirmed by code inspection: handler unguarded at
dom/ipc/WindowGlobalParent.cpp:1759. - Dynamically reproduced on a local ASAN build of 155.0a1 (the handler is identical,
WindowGlobalParent.cpp:1696). The code path is long-standing.
How it was discovered
Auditing parent-process IPC handlers for the "acts on a child-named/child-owned context without checking the sender is entitled to it" class. RecvReloadWithHttpsOnlyException stood out because it mutates the top-level context but is invoked on a per-window-global actor, and its neighbour RecvAddCertException/DoAddCertException does the opposite (self-scopes to the receiving window's own failed channel + own principal).
Root cause (code, 157.0a1)
WindowGlobalParent::RecvReloadWithHttpsOnlyException() (dom/ipc/WindowGlobalParent.cpp:1759) is the parent side of the async, argument-less IPC message PWindowGlobal::ReloadWithHttpsOnlyException (backing the "Continue to HTTP Site" button on about:httpsonlyerror; intended caller documented at dom/base/Document.h:2455 — "a trusted HTTPS only error page"). The handler never checks the identity of the sending window global. It operates entirely on the top of the sender's frame tree:
:1761currentURI = BrowsingContext()->Top()->GetCurrentURI();:1788origin attributes fromTopWindowContext()->DocumentPrincipal():1803-1804persists the permission"https-only-load-insecure" = LOAD_INSECURE_ALLOW_SESSION(EXPIRE_SESSION) for the top origin:1823-1825builds annsDocShellLoadStatefor thehttp://version of the top URI withSetTriggeringPrincipal(nsContentUtils::GetSystemPrincipal())andSetLoadType(LOAD_NORMAL_REPLACE);:1829-1830topBC = BrowsingContext()->Top(); topBC->LoadURI(loadState, /* setNavigating */ true);— force-navigating the top to plaintext HTTP.
There is no check that BrowsingContext() == BrowsingContext()->Top(), that the receiving document is about:httpsonlyerror, or that the sender owns the top. The DOM entry point is gated content-side — Document::CallerIsTrustedAboutHttpsOnlyError (dom/base/Document.webidl:344) — but a compromised content process bypasses that check by sending the PWindowGlobal message directly, so the parent must re-verify the sender and does not. A compromised content process that hosts any descendant frame of a top-level page can therefore send this message on its own (non-top) WindowGlobalChild and downgrade the cross-origin top.
Sibling asymmetry (shows the missing check is an oversight): the adjacent WindowGlobalParent::DoAddCertException (:1715) self-scopes to the receiving window's own GetFailedChannel() (:1716) and its own mDocumentPrincipal->OriginAttributesRef() (:1750) — a child can only affect the channel this window global actually failed. The HTTPS-only handler consults neither.
Impact
- A compromised cross-origin descendant iframe (e.g. an exploited third-party ad/widget/analytics embed running in its own content process under default Fission) forces its first-party embedder top from HTTPS to HTTP, and the persisted
LOAD_INSECURE_ALLOW_SESSIONexemption keeps it on HTTP for the session (suppresses HTTPS-First re-upgrade). - This is a transport-security downgrade / HTTPS-First mitigation bypass across an origin boundary: it strips HTTPS from a page the attacker's process should have no authority over, and (with a network position) exposes that first-party page to plaintext interception/injection.
- No user interaction; default configuration.
Honest severity (Committee decides)
I assess this as a candidate sec-high — a cross-origin, session-persistent HTTPS-First/transport-security mitigation bypass reachable from a compromised content process in the default configuration. I am not asserting a specific rating; it may reasonably be rated sec-moderate.
Honest limitations: (a) the attacker must control a process that hosts a descendant frame of the victim top (a compromised third-party embed), so it does not target arbitrary unrelated tabs; (b) sites with HSTS are likely still re-upgraded (HSTS is enforced independently of HTTPS-First), so the clearest impact is against HTTPS sites without HSTS; (c) turning the plaintext page into data theft requires a network (MITM) position — this report demonstrates the downgrade, not a full network exploit.
Steps to reproduce (PoC — attached separately)
Threat model: "a compromised content process makes a direct IPC call." The attached PoC simulates the compromised process with two small env-gated (MOZ_POC_HTTPS_DOWNGRADE) instrumentation hooks in libxul (poc13_hooks.patch) — it does not exploit a separate memory-safety bug to get there; it drives the exact IPC the handler receives:
WindowGlobalParent.cpp— a marker logging, on entry, whether the sender is the top and which top it acts on.WindowGlobalChild.cpp::OnDocumentLoaded— when the attacker-marked subframe finishes loading, it callsSendReloadWithHttpsOnlyException()on its own non-topWindowGlobalChild.
Harness: a victim top https://localhost:8443/top.html embeds a cross-site attacker iframe https://127.0.0.1:8443/evilframe.html (distinct site → out-of-process). The harness sets fission.webContentIsolationStrategy=1 and dom.security.https_first=true — both are the shipping defaults (1 = IsolateEverything, every cross-site frame gets its own process; verified in StaticPrefList.yaml), i.e. no non-default configuration is used. Built as an ASAN mozilla-central build; run headless.
Observed (full log in poc13_https_evidence.txt):
[VICTIM-TOP] loaded #1 scheme=https: https://localhost:8443/top.html
[PARENT] invoked by senderWGP isTop=0 senderDoc=https://127.0.0.1:8443/evilframe.html --ACTS-ON--> TOP=https://localhost:8443/top.html
[PARENT] DOWNGRADE APPLIED: persisted LOAD_INSECURE_ALLOW_SESSION for top origin + forced top LoadURI(http://localhost:8443/top.html)
[VICTIM-TOP] loaded #1 scheme=http: http://localhost:8443/top.html <- HTTPS -> HTTP
[VICTIM-TOP] loaded #2..#15 scheme=http: <- stays HTTP despite https_first=true (default)
isTop=0 proves the sender is a subframe, not the top; senderDoc (127.0.0.1) ≠ TOP (localhost) proves it is cross-origin; the scheme timeline proves the real HTTPS→HTTP downgrade and its session persistence.
Suggested fix
In RecvReloadWithHttpsOnlyException, reject the message unless the sending window global is the top-level context and is displaying the HTTPS-only error page — e.g. if (BrowsingContext() != BrowsingContext()->Top()) return IPC_OK(); plus a check that the document is about:httpsonlyerror — mirroring how DoAddCertException binds to the receiving window's own state. (An IPC_FAIL for a non-top sender would also flag the compromised process.)
Root-cause analysis vs ASAN
This is a logic/authorization bug, not memory corruption, so there is no ASAN stack; the root-cause analysis above is provided in lieu (the program requires "at least one of" ASAN trace or root-cause analysis).
Tools / environment
- mozilla-central worktrees: 157.0a1 (code inspection) and 155.0a1 (local ASAN build for the runtime PoC).
- Build:
--enable-address-sanitizer --enable-optimize(mach build); run viamach run --headless. - OS: macOS 26 (Darwin 25.6.0), arm64.
- Cross-referencing:
searchfox-cli,rg.
Updated•2 months ago
|
Updated•2 months ago
|
Description
•