Closed Bug 2067696 Opened 2 months ago Closed 2 months ago

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)

defect

Tracking

()

RESOLVED DUPLICATE of bug 2014104

People

(Reporter: part23, Unassigned)

Details

(Keywords: reporter-external, Whiteboard: [client-bounty-form])

Attachments

(1 file)

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:

  • :1761 currentURI = BrowsingContext()->Top()->GetCurrentURI();
  • :1788 origin attributes from TopWindowContext()->DocumentPrincipal()
  • :1803-1804 persists the permission "https-only-load-insecure" = LOAD_INSECURE_ALLOW_SESSION (EXPIRE_SESSION) for the top origin
  • :1823-1825 builds an nsDocShellLoadState for the http:// version of the top URI with SetTriggeringPrincipal(nsContentUtils::GetSystemPrincipal()) and SetLoadType(LOAD_NORMAL_REPLACE); :1829-1830 topBC = 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_SESSION exemption 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:

  1. WindowGlobalParent.cpp — a marker logging, on entry, whether the sender is the top and which top it acts on.
  2. WindowGlobalChild.cpp::OnDocumentLoaded — when the attacker-marked subframe finishes loading, it calls SendReloadWithHttpsOnlyException() on its own non-top WindowGlobalChild.

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 via mach run --headless.
  • OS: macOS 26 (Darwin 25.6.0), arm64.
  • Cross-referencing: searchfox-cli, rg.
Flags: sec-bounty?
Attached file poc13_bundle.tar.gz —
Group: firefox-core-security
Status: UNCONFIRMED → RESOLVED
Closed: 2 months ago
Component: Security → DOM: Security
Duplicate of bug: 2014104
Product: Firefox → Core
Resolution: --- → DUPLICATE
Flags: sec-bounty? → sec-bounty-
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: