Closed Bug 2058336 Opened 3 months ago Closed 3 months ago

Content process can disable HTTPS-First for an arbitrary host

Categories

(Core :: DOM: Security, defect)

defect

Tracking

()

RESOLVED DUPLICATE of bug 2014104

People

(Reporter: vanirxxx, Unassigned)

Details

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

Attachments

(1 file)

67.19 KB, application/x-zip-compressed
Details
Attached file poc.zip —

Summary

A content process reports its own top-level location to the parent, unvalidated, and the parent stores it. A second message then asks the parent to add an HTTPS-Only/HTTPS-First exception — it carries no arguments, so the parent takes the target host from that stored value. The exception it writes, LOAD_INSECURE_ALLOW_SESSION, is honoured in every upgrade mode, including HTTPS-First, which is on by default. A content process can therefore turn off HTTPS upgrading for any origin, for the rest of the session, without user interaction. Demonstrated: http://victim.example — never visited — stops being upgraded.

Threat model

An attacker with code execution in an ordinary content process. Page JS alone cannot do this: the location a normal renderer reports comes from its own docshell, and the exception message is sent only by the HTTPS-Only error page.

Affects

Firefox Nightly 154.0a1, self-built from mozilla-central (ASan+fuzzing), BuildID 20260716060151, Linux x86_64; line numbers are against changeset 79223b6429f6. Default configuration: dom.security.https_first is true; dom.security.https_only_mode is false and is not required. No platform conditional in the chain.

Root cause

1. The reported location is stored without validation. dom/ipc/BrowserParent.cpp:3023:

  browsingContext->SetCurrentRemoteURI(aLocation);

aLocation is the URI the child sent. ReceiveProgressListenerData above it confirms the browsing context is one this BrowserParent manages, but nothing checks that the reported URI is one the sending process may claim — a process holding http://al.com can report https://victim.example.

2. The exception message carries no arguments, so it inherits that value. dom/ipc/PWindowGlobal.ipdl:226:

  async ReloadWithHttpsOnlyException();

dom/ipc/WindowGlobalParent.cpp:1653:

  nsCOMPtr<nsIURI> currentURI = BrowsingContext()->Top()->GetCurrentURI();

The handler rewrites that URI's scheme to http and, at dom/ipc/WindowGlobalParent.cpp:1694, grants the permission:

  rv = permMgr->AddFromPrincipal(
      principal, "https-only-load-insecure"_ns,
      nsIHttpsOnlyModePermission::LOAD_INSECURE_ALLOW_SESSION,
      nsIPermissionManager::EXPIRE_SESSION, 0);

There is no user-gesture requirement, and no check that the current URI is related to the sending window's principal.

3. The value written is not scoped to HTTPS-Only. dom/security/nsHTTPSOnlyUtils.cpp:709:

  return perm == nsIHttpsOnlyModePermission::LOAD_INSECURE_ALLOW ||
         perm == nsIHttpsOnlyModePermission::LOAD_INSECURE_ALLOW_SESSION ||
         (checkForHTTPSFirst &&
          perm == nsIHttpsOnlyModePermission::HTTPSFIRST_LOAD_INSECURE_ALLOW);

Only HTTPSFIRST_LOAD_INSECURE_ALLOW is qualified by checkForHTTPSFirst. LOAD_INSECURE_ALLOW_SESSION — the value step 2 writes — exempts the origin in all upgrade modes, so the grant intended for opt-in HTTPS-Only also disables the default-on HTTPS-First upgrade.

Steps to reproduce

From a content process holding http://al.com:8199: send OnLocationChange for its own top-level browsing context with aLocation = https://victim.example/, then send ReloadWithHttpsOnlyException on its window global. Then load http://victim.example/.

Actual result

The parent grants the exception for a host the browser never visited, and the next load of that host is not upgraded:

[FORGE-LOC] reporting https://victim.example/ instead of about:blank
[FORGE-EXC] sending ReloadWithHttpsOnlyException
[PARENT-GRANT] https-only-load-insecure for http://victim.example
[EXEMPT] upgrade skipped for http://victim.example (mode=2 perm=9)

mode=2 is HTTPS_FIRST_MODE; perm=9 is LOAD_INSECURE_ALLOW_SESSION. Reproduced 3/3. With the two forged messages removed and nothing else changed, neither line appears at all (0 occurrences of both).

Expected result

The target of an HTTPS-Only/HTTPS-First exception must be derived from state the parent owns for the sending window — its document principal — not from a location string the same process reported earlier, and the grant should be scoped to the mode it was issued for.

Proof of concept

poc/ — two env-gated hunks in the child modelling the compromised renderer (report a chosen location; send the exception message), two read-only prints in the parent (the permission grant, and the point where the upgrade is skipped), the test page, and RESULTS.txt with the attack next to its control.

Suggested fix

  • Validate in RecvOnLocationChange that the reported URI is loadable by the sending process, as other content→parent URI handlers do.
  • Derive the exception target in RecvReloadWithHttpsOnlyException from DocumentPrincipal() rather than from GetCurrentURI(), and require a user gesture.
  • Gate LOAD_INSECURE_ALLOW_SESSION on the mode it was granted in, the way HTTPSFIRST_LOAD_INSECURE_ALLOW already is.
Flags: sec-bounty?
Group: firefox-core-security → dom-core-security
Status: UNCONFIRMED → RESOLVED
Closed: 3 months ago
Component: Security → DOM: Security
Duplicate of bug: 2014104
Product: Firefox → Core
Resolution: --- → DUPLICATE
Group: dom-core-security
Flags: sec-bounty? → sec-bounty-
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: