Closed Bug 2054946 Opened 4 months ago Closed 4 months ago

RecvReloadWithHttpsOnlyException missing IsTop check lets iframe force top-level HTTPS to HTTP downgrade

Categories

(Core :: DOM: Security, defect)

defect

Tracking

()

RESOLVED DUPLICATE of bug 2014104

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:

  1. User visits https://bank.example with HTTPS-Only mode enabled
  2. bank.example embeds a cross-origin ad iframe from ad-network.example
  3. Attacker compromises the ad iframe's content process (for example through
    a separate renderer bug targeting the ad network)
  4. The compromised process calls SendReloadWithHttpsOnlyException() on its
    WindowGlobalChild
  5. The parent handler at line 1651 runs without an IsTop() check
  6. Line 1653: Gets the top-level URI (https://bank.example) via
    BrowsingContext()->Top()->GetCurrentURI()
  7. Line 1676: Mutates the scheme from https to http
  8. Line 1694: Adds a session permission "https-only-load-insecure" for
    bank.example, allowing insecure loads for the rest of the session
  9. Line 1716: Sets SystemPrincipal as the triggering principal
  10. Line 1722: Reloads the top-level browsing context (bank.example) over HTTP
  11. The user's bank page is now loaded over HTTP
  12. A network attacker (public WiFi, compromised router, ISP injection) can
    now intercept and modify the bank page traffic
  13. 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:

  1. HTTPS downgrade: The top-level page reloads over HTTP
  2. Persistent for session: The "https-only-load-insecure" permission
    persists for the entire browser session (EXPIRE_SESSION at line 1697)
  3. 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.

Flags: sec-bounty?
Status: UNCONFIRMED → RESOLVED
Closed: 4 months ago
Duplicate of bug: 2014104
Resolution: --- → DUPLICATE
Group: firefox-core-security
Component: Security → DOM: Security
Product: Firefox → Core
Flags: sec-bounty? → sec-bounty-
You need to log in before you can comment on or make changes to this bug.