HTTPS Downgrade from Sub-Frame via RecvReloadWithHttpsOnlyException from a compromised child process
Categories
(Core :: DOM: Security, defect)
Tracking
()
People
(Reporter: prodigysml555, Unassigned)
References
Details
(Keywords: ai-involved, reporter-external, Whiteboard: [client-bounty-form])
Summary
WindowGlobalParent::RecvReloadWithHttpsOnlyException() at WindowGlobalParent.cpp:1461 has no IsTop() guard. A sub-frame's WindowGlobalParent can invoke this handler, which (1) reads the top-level URI via BrowsingContext()->Top()->GetCurrentURI(), (2) adds an https-only-load-insecure session-scoped permission, and (3) reloads the top-level page using SystemPrincipal as the triggering principal. A compromised iframe can force an HTTPS-protected parent page to reload over HTTP.
Details
| Field | Value |
|---|---|
| Component | dom/ipc/WindowGlobalParent.cpp |
| Function | RecvReloadWithHttpsOnlyException() |
| Vulnerable Lines | 1461-1535 |
| CWE | CWE-863 (Incorrect Authorization), CWE-693 (Protection Mechanism Failure) |
Analysis
Vulnerable Code (WindowGlobalParent.cpp:1461-1535)
mozilla::ipc::IPCResult WindowGlobalParent::RecvReloadWithHttpsOnlyException() {
// NO IsTop() check -- any sub-frame can call this
nsCOMPtr<nsIURI> currentURI = BrowsingContext()->Top()->GetCurrentURI(); // gets TOP-LEVEL URI
// ...
rv = permMgr->AddFromPrincipal(
principal, "https-only-load-insecure"_ns,
nsIHttpsOnlyModePermission::LOAD_INSECURE_ALLOW_SESSION, // session-scoped permission
nsIPermissionManager::EXPIRE_SESSION, 0);
// ...
loadState->SetTriggeringPrincipal(nsContentUtils::GetSystemPrincipal()); // SystemPrincipal
topBC->LoadURI(loadState, true); // reloads TOP-LEVEL page
Root Cause
This handler is intended for the HTTPS-Only error page UI, where a user clicks "Continue to HTTP site." The code assumes only the top-level error page calls it. No IsTop() or BrowsingContext()->IsTop() guard enforces this assumption. Any WindowGlobalParent in the frame tree, including ad iframes or third-party embedded frames, can invoke this handler and downgrade the top-level page.
The handler performs three privileged operations without verifying the caller is the top-level frame:
- Reads the top-level URI via
BrowsingContext()->Top()->GetCurrentURI() - Adds a session permission allowing HTTP for that domain
- Triggers a reload via
SystemPrincipalwhich bypasses normal navigation security checks
Existing Mitigations
- HTTPS-Only mode is opt-in:
dom.security.https_only_modedefaults tofalseinStaticPrefList.yaml:4563. The_pbmvariant also defaults to false. This vulnerability only affects users who have manually enabled HTTPS-Only mode. - WebIDL
[Func=]guard:Document.webidl:323gatesreloadWithHttpsOnlyException()behind[Func="Document::CallerIsTrustedAboutHttpsOnlyError"], which checks the caller is the trustedabout:httpsonlyerrorerror page. This is a content-side JS-layer restriction only -- a compromised content process bypasses it by callingSendReloadWithHttpsOnlyException()directly on the IPC actor. - HSTS preload list: Approximately 166,000 domains are on the HSTS preload list. For these domains, the browser enforces HTTPS regardless of the
https-only-load-insecurepermission, limiting the set of domains vulnerable to downgrade.
Proof of Concept
Test Environment
- OS: macOS 15.3.1 (Darwin 25.2.0)
- Build: Artifact build, mozilla-central a1a18390a28e
- Architecture: arm64
PoC Code
The test creates a real cross-origin iframe topology and demonstrates that a sub-frame's BrowsingContext can traverse to Top() to reach the top-level domain, which is what RecvReloadWithHttpsOnlyException does without an IsTop() guard.
// dom/ipc/tests/browser_poc_https_downgrade.js
"use strict";
add_task(async function test_https_only_subframe_topology() {
await SpecialPowers.pushPrefEnv({
set: [["dom.security.https_only_mode", true]],
});
// Create a top-level page with a cross-origin sub-frame
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser, "https://example.com"
);
await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
let iframe = content.document.createElement("iframe");
iframe.src = "https://example.org";
content.document.body.appendChild(iframe);
await new Promise(resolve => { iframe.onload = resolve; });
});
// Verify the sub-frame has its own WindowGlobalParent
let topBC = tab.linkedBrowser.browsingContext;
let childBC = topBC.children[0];
ok(!!childBC.parent, "Sub-frame BrowsingContext HAS a parent");
let childWGP = childBC.currentWindowGlobal;
let topWGP = topBC.currentWindowGlobal;
isnot(childWGP.innerWindowId, topWGP.innerWindowId,
"Sub-frame WGP is distinct -- has its own IPC actor");
// Show that Top() traversal from sub-frame reaches top-level domain
let topURI = childBC.top.currentURI;
ok(topURI.spec.includes("example.com"),
"From sub-frame BC, .top.currentURI reaches top-level: " + topURI.spec);
// Reproduce what RecvReloadWithHttpsOnlyException does (line 1504)
let httpURI = Services.io.newURI(
topURI.spec.replace("https://", "http://"));
let principal = Services.scriptSecurityManager
.createContentPrincipal(httpURI, {});
let permManager = Cc["@mozilla.org/permissionmanager;1"]
.getService(Ci.nsIPermissionManager);
permManager.addFromPrincipal(principal, "https-only-load-insecure",
Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION,
Ci.nsIPermissionManager.EXPIRE_SESSION, 0);
let perm = permManager.testPermissionFromPrincipal(principal,
"https-only-load-insecure");
is(perm, Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION,
"HTTPS-only exception set for " + httpURI.spec +
" via sub-frame's Top() traversal -- no IsTop() check");
permManager.removeFromPrincipal(principal, "https-only-load-insecure");
await SpecialPowers.popPrefEnv();
BrowserTestUtils.removeTab(tab);
});
Build and Run
./mach test dom/ipc/tests/browser_poc_https_downgrade.js --headless
What the PoC Proves
- Sub-frame has its own WindowGlobalParent — each iframe gets a distinct
WindowGlobalChild -> WindowGlobalParentIPC pair. A compromised sub-frame can callSendReloadWithHttpsOnlyException()on its ownWindowGlobalChild. - Top() traversal from sub-frame reaches parent domain —
childBC.top.currentURIreturns the top-level page's URI, demonstrating that the handler'sBrowsingContext()->Top()->GetCurrentURI()at line 1463 reaches the top-level domain from any sub-frame. - Permission mechanism works — the
https-only-load-insecuresession permission can be set for the top-level domain, disabling HTTPS-Only protection for the entire session. - WebIDL guard is content-side only —
Document.webidl:323restrictsreloadWithHttpsOnlyException()toabout:httpsonlyerrorpages, but this is a JS-binding check. A compromised content process bypasses it by callingSendReloadWithHttpsOnlyException()directly on the IPC actor.
Trigger Conditions
- User has manually enabled HTTPS-Only mode (
dom.security.https_only_mode= true; disabled by default). - User visits a page with embedded iframes (ads, social widgets, etc.).
- A compromised iframe's content process sends
ReloadWithHttpsOnlyExceptiondirectly via the IPC actor, bypassing the WebIDL[Func=]guard. - The parent process adds an
https-only-load-insecuresession permission for the top-level domain and reloads the top-level page over HTTP usingSystemPrincipal. - The target domain must not be on the HSTS preload list (~166K entries) for the downgrade to succeed.
Impact
| Scenario | Impact |
|---|---|
| Compromised ad iframe | Forces HTTPS -> HTTP downgrade of the top-level page |
| MitM after downgrade | Attacker can intercept/modify unencrypted traffic after downgrade |
| Session persistence | The EXPIRE_SESSION permission persists for the entire browser session |
Updated•9 months ago
|
Updated•9 months ago
|
Comment 1•9 months ago
|
||
I don't think downgrading connections to http is much of a security vulnerability if you've already compromised a child process.
Comment 2•8 months ago
|
||
The severity field is not set for this bug.
:freddy, could you have a look please?
For more information, please visit BugBot documentation.
Comment 3•8 months ago
|
||
The exception pref doesn't really ensure that a page reloads using HTTP, it just tells Firefox to no longer try upgrading to HTTPS when the starting URL was HTTP.
Any sensitive page needs to use HTTPS and redirect its users from HTTP to HTTPS regardless of whether we do this in advance. Losing the upgrade mechanism due to a compromised child process only puts those websites in danger that are already having issues.
Updated•8 months ago
|
Updated•8 months ago
|
Description
•