RecvReloadWithHttpsOnlyException missing IsTop check lets iframe force top-level HTTPS to HTTP downgrade
Categories
(Core :: DOM: Security, defect)
Tracking
()
People
(Reporter: dolar199930, Unassigned)
References
()
Details
(Keywords: reporter-external, Whiteboard: [client-bounty-form])
RecvReloadWithHttpsOnlyException missing IsTop check lets iframe force top-level HTTPS to HTTP downgrade
Summary
RecvReloadWithHttpsOnlyException at WindowGlobalParent.cpp:1651 does not check
whether the calling WindowGlobal is the top-level browsing context. At least
6 other handlers in the same file perform this check. The handler operates on
BrowsingContext()->Top(), meaning a compromised cross-origin iframe content
process can force the entire top-level page to reload over HTTP with
SystemPrincipal, creating a session permission that allows insecure loads
for that domain.
Affected Code
File: dom/ipc/WindowGlobalParent.cpp, lines 1651-1724
mozilla::ipc::IPCResult WindowGlobalParent::RecvReloadWithHttpsOnlyException() {
nsresult rv;
nsCOMPtr<nsIURI> currentURI = BrowsingContext()->Top()->GetCurrentURI();
if (!currentURI) {
return IPC_FAIL(this, "HTTPS-only mode: Failed to get current URI");
}
// ... URI manipulation ...
// We replace the scheme with http
nsCOMPtr<nsIURI> newURI;
(void)NS_MutateURI(innerURI).SetScheme("http"_ns).Finalize(
getter_AddRefs(newURI));
// ... creates principal and adds permission ...
rv = permMgr->AddFromPrincipal(
principal, "https-only-load-insecure"_ns,
nsIHttpsOnlyModePermission::LOAD_INSECURE_ALLOW_SESSION,
nsIPermissionManager::EXPIRE_SESSION, 0);
// ... loads with SystemPrincipal ...
RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(insecureURI);
loadState->SetTriggeringPrincipal(nsContentUtils::GetSystemPrincipal());
loadState->SetLoadType(LOAD_NORMAL_REPLACE);
RefPtr<CanonicalBrowsingContext> topBC = BrowsingContext()->Top();
topBC->LoadURI(loadState, /* setNavigating */ true);
return IPC_OK();
}
No IsTop() check at the start of the handler. The handler gets the top-level
URI (line 1653), mutates it to HTTP (line 1676), adds a session permission
for insecure loads (line 1694), and reloads the top-level browsing context
with SystemPrincipal (lines 1716, 1722).
The Missing IsTop Check
At least 6 other handlers in the same file check IsTop() or IsTopContent():
| Line | Handler | Check |
|---|---|---|
| 243 | Init (content blocking) | BrowsingContext()->IsTopContent() |
| 538 | RecvUpdateDocumentWouldLoadBadly | !BrowsingContext()->IsTop() |
| 1194 | RecvDiscoverIdentityCredentialFromExternalSource | MOZ_DIAGNOSTIC_ASSERT(BrowsingContext()->IsTop()) |
| 1766 | RecvGetContentBlockingEvents | GetBrowsingContext()->IsTopContent() |
| 1781 | RecvUpdateCookieJarSettings | GetBrowsingContext()->IsTopContent() |
| 1895 | RecvSetContainerFeaturePolicy | !BrowsingContext()->IsTopContent() |
RecvReloadWithHttpsOnlyException does not have this check, even though it
operates on BrowsingContext()->Top() and affects the top-level page.
Child-Side Gate Is Irrelevant for Compromised Processes
The child-side caller is gated by WebIDL:
File: dom/webidl/Document.webidl, lines 336-337
[Func="Document::CallerIsTrustedAboutHttpsOnlyError"]
undefined reloadWithHttpsOnlyException();
This restricts the JavaScript API to the about:httpsonlyerror error page only.
Normal web content cannot call document.reloadWithHttpsOnlyException().
But a compromised content process does not go through WebIDL. It calls
SendReloadWithHttpsOnlyException() directly on its WindowGlobalChild, bypassing
the trusted page check entirely. The WebIDL gate protects against untrusted
JavaScript, not against a compromised C++ process.
The only child-side caller:
File: dom/base/Document.cpp, lines 1725-1729
void Document::ReloadWithHttpsOnlyException() {
if (WindowGlobalChild* wgc = GetWindowGlobalChild()) {
wgc->SendReloadWithHttpsOnlyException();
}
}
No IsTop() check on the child side either.
Steps to Reproduce
This requires a compromised content process and a network attacker position:
- User visits https://bank.example with HTTPS-Only mode enabled
- bank.example embeds a cross-origin ad iframe from ad-network.example
- Attacker compromises the ad iframe's content process (for example through
a separate renderer bug targeting the ad network) - The compromised process calls SendReloadWithHttpsOnlyException() on its
WindowGlobalChild - The parent handler at line 1651 runs without an IsTop() check
- Line 1653: Gets the top-level URI (https://bank.example) via
BrowsingContext()->Top()->GetCurrentURI() - Line 1676: Mutates the scheme from https to http
- Line 1694: Adds a session permission "https-only-load-insecure" for
bank.example, allowing insecure loads for the rest of the session - Line 1716: Sets SystemPrincipal as the triggering principal
- Line 1722: Reloads the top-level browsing context (bank.example) over HTTP
- The user's bank page is now loaded over HTTP
- A network attacker (public WiFi, compromised router, ISP injection) can
now intercept and modify the bank page traffic - The session permission at line 1694 means HTTPS-Only mode will not
re-upgrade the connection for this domain until the session ends
Security Impact
A cross-origin iframe content process can downgrade the top-level page from
HTTPS to HTTP. This breaks the security guarantee that HTTPS-Only mode is
supposed to provide. The attack has three components:
- HTTPS downgrade: The top-level page reloads over HTTP
- Persistent for session: The "https-only-load-insecure" permission
persists for the entire browser session (EXPIRE_SESSION at line 1697) - SystemPrincipal triggering: The reload uses SystemPrincipal (line 1716),
bypassing any content-level security checks on the navigation
Combined with a network position (public WiFi, compromised network), this
enables full man-in-the-middle against the top-level origin.
Root Cause
The handler was written to support the HTTPS-Only error page UI button, where
the user explicitly clicks "Continue to HTTP Site." In that context, the
calling WindowGlobal is always the top-level error page itself. The developer
did not add an IsTop() check because the WebIDL gate (CallerIsTrustedAboutHttpsOnlyError)
was supposed to restrict access to the error page.
Under Firefox's content process compromise threat model, the WebIDL gate is
not sufficient. A compromised iframe content process can call the IPC message
directly, and the parent handler must independently validate that the caller
is the top-level context.
Suggested Fix
Add an IsTop() check to match the pattern used by adjacent handlers:
mozilla::ipc::IPCResult WindowGlobalParent::RecvReloadWithHttpsOnlyException() {
if (!BrowsingContext()->IsTop()) {
return IPC_FAIL(this,
"HTTPS-only mode: Exception can only be set by top-level context");
}
nsresult rv;
nsCOMPtr<nsIURI> currentURI = BrowsingContext()->Top()->GetCurrentURI();
// ... rest unchanged
This matches the validation pattern used by at least 6 other handlers in
the same file and prevents cross-origin iframes from affecting the top-level
page's HTTPS status.
Environment
- Source: mozilla-firefox/firefox (HEAD as of 2026-07-14)
- Affected files:
- dom/ipc/WindowGlobalParent.cpp (handler, lines 1651-1724)
- dom/webidl/Document.webidl (WebIDL gate, lines 336-337)
- dom/base/Document.cpp (child-side sender, lines 1725-1729)
- Discovery method: Manual source code audit comparing IsTop() check
patterns across handlers in WindowGlobalParent.cpp. Found 6+ handlers
with the check and this handler without it.
Additional Notes
The handler creates a session-scoped permission (EXPIRE_SESSION) rather than
a one-time exception. This means the HTTPS downgrade persists for the entire
browser session. If the user navigates away from bank.example and comes back,
HTTPS-Only mode will still allow the insecure load because the session
permission is still active.
The SystemPrincipal triggering at line 1716 is particularly concerning because
it gives the reload the highest privilege level. If there were any other
security checks on the navigation path that key on the triggering principal,
SystemPrincipal would bypass all of them.
Updated•4 months ago
|
Updated•4 months ago
|
Updated•3 months ago
|
Description
•