Closed Bug 2026788 Opened 7 months ago Closed 7 months ago

Compromised content process can spoof URL bar and TLS security indicators via RecvOnLocationChange + RecvUpdateDocumentSecurityInfo

Categories

(Core :: DOM: Security, defect)

defect

Tracking

()

RESOLVED DUPLICATE of bug 2014254

People

(Reporter: mirzashihab2, Unassigned)

Details

(Keywords: ai-involved, reporter-external)

Attachments

(1 file)

Attached file poc_urlbar_spoofing.py —

Compromised content process can spoof URL bar and TLS security indicators via RecvOnLocationChange + RecvUpdateDocumentSecurityInfo

Summary

A compromised content process can fully spoof Firefox's URL bar and TLS security indicators (padlock icon, EV certificate badge, certificate details) by sending two IPC messages with no validation:

  1. PWindowGlobal::UpdateDocumentSecurityInfo — replaces TLS security info with forged data (zero validation)
  2. PBrowser::OnLocationChange — sets the displayed URL to any URI (URI itself never validated)

Combined, these allow a compromised renderer on an attacker-controlled HTTPS site to display https://bank.com in the URL bar with a green padlock and forged EV certificate details, while the actual page content is the attacker's phishing form. This is a complete bypass of Fission's browser trust indicator isolation model.

Prerequisite: The attacker's own site must use HTTPS (free via Let's Encrypt). This is because the lock icon decision reads GetIsSecure() (based on the WindowGlobal's document URI scheme), which is already true for any HTTPS page.

Affected Component

  • dom/ipc/BrowserParent.cpp, lines 2981-3034 (RecvOnLocationChange — URI not validated)
  • dom/ipc/WindowGlobalParent.cpp, lines 729-733 (RecvUpdateDocumentSecurityInfo — zero validation)
  • docshell/base/CanonicalBrowsingContext.cpp (SetCurrentRemoteURI — direct assignment, zero checks)
  • security/manager/ssl/nsSecureBrowserUI.cpp, lines 51-85 (RecomputeSecurityFlags — reads forged mSecurityInfo)
  • browser/base/content/browser-siteIdentity.js (displays lock icon from forged data)

Severity

sec-critical (S1) — A compromised content process can perfectly spoof the browser's URL bar and all TLS security indicators, enabling phishing attacks that are visually indistinguishable from legitimate HTTPS sites. This directly violates the Fission security model, which is designed to prevent a compromised renderer from spoofing trusted browser UI.

Root Cause

Issue 1: RecvOnLocationChange does not validate the URI

// dom/ipc/BrowserParent.cpp:2981-3034
IPCResult BrowserParent::RecvOnLocationChange(
    const WebProgressData& aWebProgressData, const RequestData& aRequestData,
    nsIURI* aLocation, /* ... */
    const Maybe<WebProgressLocationChangeData>& aLocationChangeData) {

  // ReceiveProgressListenerData checks browsing context ownership only
  if (!ReceiveProgressListenerData(...)) { return IPC_OK(); }

  // Content-controlled URI stored directly with NO validation
  browsingContext->SetCurrentRemoteURI(aLocation);

  // Content-controlled principal passed to browser element
  if (aLocationChangeData.isSome()) {
    browser->UpdateForLocationChange(
        aLocation, ...,
        aLocationChangeData->contentPrincipal(),     // content-controlled
        aLocationChangeData->contentPartitionedPrincipal(), ...);
  }

  // Fires OnLocationChange to web progress listeners (updates URL bar)
  listener->OnLocationChange(listener, request, aLocation, aFlags);

  // Updates security state using the forged mSecurityInfo
  if (browsingContext->IsTopContent() &&
      !(aFlags & LOCATION_CHANGE_SAME_DOCUMENT)) {
    browsingContext->UpdateSecurityState();
  }
}

SetCurrentRemoteURI (CanonicalBrowsingContext.cpp) has zero checks:

void CanonicalBrowsingContext::SetCurrentRemoteURI(nsIURI* aCurrentRemoteURI) {
  MOZ_ASSERT(!GetDocShell());
  mCurrentRemoteURI = aCurrentRemoteURI;  // Direct assignment
}

GetCurrentURI() returns this for remote (Fission) browsing contexts:

already_AddRefed<nsIURI> CanonicalBrowsingContext::GetCurrentURI() const {
  if (nsIDocShell* docShell = GetDocShell()) { /* local path */ }
  else { currentURI = mCurrentRemoteURI; }  // Returns spoofed URI
}

The URL bar (UrlbarInput.mjs) reads gBrowser.currentURI which calls GetCurrentURI().

Issue 2: RecvUpdateDocumentSecurityInfo has zero validation

// dom/ipc/WindowGlobalParent.cpp:729-733
IPCResult WindowGlobalParent::RecvUpdateDocumentSecurityInfo(
    nsITransportSecurityInfo* aSecurityInfo) {
  mSecurityInfo = aSecurityInfo;  // Zero validation
  return IPC_OK();
}

Issue 3: Lock icon reads the forged security info

// security/manager/ssl/nsSecureBrowserUI.cpp:51-85
void nsSecureBrowserUI::RecomputeSecurityFlags() {
  RefPtr<WindowGlobalParent> win = GetCurrentWindow();
  mState = STATE_IS_INSECURE;

  if (win && win->GetIsSecure()) {       // true if documentURI scheme is "https"
    securityInfo = win->GetSecurityInfo(); // returns forged mSecurityInfo
    if (securityInfo) {
      securityInfo->GetSecurityState(&mState);  // STATE_IS_SECURE from forged data
      securityInfo->GetIsExtendedValidation(&isEV); // true from forged data
      if (isEV) {
        mState |= STATE_IDENTITY_EV_TOPLEVEL;  // EV badge enabled
      }
    }
  }
}

GetIsSecure() (WindowGlobalActor.cpp:136-138) checks if the WindowGlobal's documentURI scheme is "https". Since the attacker's own site is HTTPS, this is already true — the attacker does not need to forge this field.

Issue 4: Parent process never cross-checks

In Fission, the parent process handles network requests via DocumentChannel/DocumentLoadListener. It has the real URI and real TLS info during the request. But:

  • The parent-side TLS info is not preserved for later comparison
  • SetCurrentRemoteURI is not compared against the DocumentChannel URI
  • mSecurityInfo is not compared against the network layer's TLS info
  • The content process's report is accepted as authoritative for both the URI and security state

Attack Flow

Prerequisite

Attacker controls https://evil.com (free Let's Encrypt certificate).

Step 1: User visits attacker's HTTPS site

User navigates to https://evil.com. The WindowGlobal is created with:

  • documentURI = "https://evil.com" → GetIsSecure() = true
  • mSecurityInfo = evil.com's legitimate TLS info

Content process is compromised (e.g., via renderer exploit).

Step 2: Forge TLS security info

Compromised content process sends:

PWindowGlobal::UpdateDocumentSecurityInfo(
    securityInfo = {
        securityState: STATE_IS_SECURE,
        isExtendedValidation: true,
        serverCert: {
            commonName: "www.bank.com",
            organization: "Bank of America, Inc.",
            issuerOrganization: "DigiCert Inc",
            sha256Fingerprint: "AA:BB:CC:...(forged)..."
        },
        cipherName: "TLS_AES_256_GCM_SHA384",
        protocolVersion: TLS_VERSION_1_3
    }
)

Parent: mSecurityInfo = aSecurityInfo; — stored with zero checks.

Step 3: Spoof URL bar and trigger security state update

Compromised content process sends:

PBrowser::OnLocationChange(
    browsingContext = <this tab's top-level BC>,
    location = "https://bank.com/login",
    flags = 0,  // NOT LOCATION_CHANGE_SAME_DOCUMENT — triggers UpdateSecurityState
    locationChangeData = {
        contentPrincipal: <principal for https://bank.com>,
        title: "Bank of America - Sign In",
        documentURI: "https://bank.com/login"
    }
)

Parent process executes:

  1. SetCurrentRemoteURI("https://bank.com/login") — URL stored (no validation)
  2. UpdateForLocationChange(...) — browser element updated with spoofed data
  3. OnLocationChange(...) → URL bar shows https://bank.com/login
  4. UpdateSecurityState() → RecomputeSecurityFlags():
    • GetIsSecure() = true (documentURI https://evil.com is HTTPS)
    • GetSecurityInfo() = forged mSecurityInfo (bank.com EV cert)
    • GetSecurityState() → STATE_IS_SECURE
    • GetIsExtendedValidation() → true
    • mState |= STATE_IDENTITY_EV_TOPLEVEL
  5. Lock icon: green padlock with EV badge

Result

User sees:

  • URL bar: https://bank.com/login
  • Lock icon: Green padlock
  • EV badge: "Bank of America, Inc. [US]"
  • Certificate viewer: Forged certificate details for bank.com
  • Page content: Attacker's phishing form

Attack requires NO user interaction beyond having visited the attacker's HTTPS page.

Impact

  • Perfect phishing: Visually indistinguishable from real HTTPS site with EV certificate
  • Credential theft: User enters passwords for spoofed site
  • Principal spoofing: contentPrincipal in UpdateForLocationChange is content-controlled
  • Undetectable: No visual indicator that anything is wrong — all trust signals are forged
  • Low barrier: Attacker only needs a free Let's Encrypt cert for their own site
  • All platforms: Affects all platforms with Fission enabled (default on all desktop platforms)

Mitigations Checked and Found Insufficient

Mitigation Result
ReceiveProgressListenerData (BrowserParent.cpp:3120) Checks browsing context ownership only, NOT the URI
SetCurrentRemoteURI (CanonicalBrowsingContext.cpp) Direct assignment, MOZ_ASSERT only
DocumentChannel parent-side URI Never compared against content-reported URI
TLS info from network layer Parent does not store its own TLS info for comparison
GetIsSecure() (WindowGlobalActor.cpp:137) Checks documentURI scheme — true for attacker's HTTPS site
ValidatePrincipal in RecvNewWindowGlobal (BrowserParent.cpp:1401) Non-fatal: logs + MOZ_ASSERT only, execution continues
Fission process isolation Content process reports to its OWN tab — ownership check passes

Suggested Fix

Fix 1: Parent-authoritative URI for URL bar

The parent process should set the URL bar URI from its own navigation data (DocumentChannel), not from the content process. RecvOnLocationChange should only be allowed to report same-origin URIs matching the navigation the parent initiated.

Fix 2: Parent-controlled security info

Remove RecvUpdateDocumentSecurityInfo. The parent should derive TLS security info from the DocumentChannel's actual network connection, not from content process reports.

Fix 3: Cross-check URI against navigation

If RecvOnLocationChange reports a URI that doesn't match the last parent-initiated navigation for this browsing context, reject with IPC_FAIL.

Files

File Line Role
dom/ipc/BrowserParent.cpp 2981-3034 RecvOnLocationChange — URI not validated
dom/ipc/BrowserParent.cpp 3120-3170 ReceiveProgressListenerData — ownership check only
dom/ipc/BrowserParent.cpp 1401-1403 RecvNewWindowGlobal — principal validation non-fatal
dom/ipc/WindowGlobalParent.cpp 729-733 RecvUpdateDocumentSecurityInfo — zero validation
dom/ipc/WindowGlobalActor.cpp 136-138 GetIsSecure — checks documentURI scheme
docshell/base/CanonicalBrowsingContext.cpp SetCurrentRemoteURI Direct URI assignment
docshell/base/CanonicalBrowsingContext.cpp GetCurrentURI Returns spoofed URI for remote contexts
security/manager/ssl/nsSecureBrowserUI.cpp 51-85 Reads forged security info for lock icon
browser/base/content/browser-siteIdentity.js 713-720 Displays lock icon from forged data
browser/components/urlbar/UrlbarInput.mjs 825 URL bar reads gBrowser.currentURI
Flags: sec-bounty?
Group: core-security → dom-core-security
Status: UNCONFIRMED → RESOLVED
Closed: 7 months ago
Duplicate of bug: 2014254
Resolution: --- → DUPLICATE
Flags: sec-bounty? → sec-bounty-
Group: dom-core-security
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: