Closed Bug 2025170 (CVE-2026-8964) Opened 6 months ago Closed 5 months ago

Blocked popup report-index confusion causing UI selection mismatch

Categories

(Toolkit :: Popup Blocker, defect)

defect

Tracking

()

RESOLVED FIXED
151 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr140 --- wontfix
firefox149 --- wontfix
firefox150 --- wontfix
firefox151 + fixed

People

(Reporter: t.satoki111, Assigned: mdauer)

Details

(Keywords: csectype-spoof, reporter-external, sec-low, Whiteboard: [client-bounty-form][adv-main151+])

Attachments

(2 files)

Attached file poc_server.py —

Overview

Firefox flattens blocked popups from the entire BrowsingContext tree into one UI list, but replays them using the per-document local popup array stored by each PopupAndRedirectBlockingChild.

As a result, when blocked popups exist across multiple browsing contexts or after a non-zero offset has been introduced, the popup the user selects in browser chrome can differ from the popup that is actually reopened.

Affected area

  • Popup-blocker notification menu entries used to reopen individual blocked popups
  • unblockAllPopups() behind "Allow pop-ups for site" is affected by the same index confusion
  • Current stable-equivalent source in:
    • PopupAndRedirectBlockerObserver.sys.mjs
    • PopupAndRedirectBlockingParent.sys.mjs
    • PopupAndRedirectBlockingChild.sys.mjs

Preconditions

  • More than one blocked popup exists within a single browser/tab
  • At least one target popup belongs to a browsing context with a non-zero flattened offset
  • The user chooses a specific blocked popup from the popup-blocker UI

Conditions for exploitation

  • Parent-side code builds one flat popup list across all browsing contexts
  • UI stores the flat position as popupReportIndex
  • Parent-side replay sends that flat index back to a specific child actor
  • Child-side replay interprets it as a local index into state.popups

Reachability

  1. Blocked popups are recorded in one or more frames/documents
  2. PopupAndRedirectBlockingParent.getBlockedPopups() walks the browsing-context tree and appends popup records into one flat array
  3. PopupAndRedirectBlockerObserver.onPopupShowingBlockedPopups() assigns popupReportIndex = i from the flat array position
  4. The user clicks a menu item
  5. showBlockedPopup() / unblockPopup() forwards that flat index to the actor selected by browsingContext
  6. PopupAndRedirectBlockingChild.#unblockPopup() uses state.popups[idx] as a local index

Root cause

This is a global-index vs local-index confusion.

  • Parent/UI side:
    • popup records are flattened across the whole browser tree
    • the stored index is global
  • Child side:
    • popup state is maintained per document
    • replay expects a local index

No conversion from flattened offset to actor-local offset exists in the replay path.

Evidence

1. Flat list creation

  • firefox/toolkit/actors/PopupAndRedirectBlockingParent.sys.mjs
    • getBlockedPopups() traverses all browsing contexts and appends popup data into one result
  • firefox/browser/modules/PopupAndRedirectBlockerObserver.sys.mjs
    • onPopupShowingBlockedPopups() stores loop index i as popupReportIndex

2. Local replay

  • firefox/toolkit/actors/PopupAndRedirectBlockingParent.sys.mjs
    • unblockPopup() selects one actor using browsingContext and forwards index: aPopupIndex
  • firefox/toolkit/actors/PopupAndRedirectBlockingChild.sys.mjs
    • #unblockPopup() resolves this.#getOrCreateDocState().popups[idx]

3. Missing normalization

  • No flat-index to local-index conversion exists
  • No per-context base offset is stored in the menu item

Falsifiability

This candidate would be weakened only if another path normalized the index before it reached the child actor, but that normalization does not exist in:

  • PopupAndRedirectBlockerObserver.showBlockedPopup()
  • PopupAndRedirectBlocker.unblockPopup()
  • PopupAndRedirectBlockingChild.#unblockPopup()

The effect is not always "open the wrong popup". Depending on offsets, it can also be a no-op.

  • Let k be the number of blocked popups that appear earlier in the flattened list
  • Let j be the target popup's local index in its own document
  • The child receives k + j
  • If k + j < localPopupCount, a different popup in the same document is reopened
  • Otherwise, replay resolves to undefined and nothing opens

So exploit reliability depends on popup distribution, but the index confusion itself is directly supported by code.

Why this is not acceptable behavior

The popup-blocker menu is a security-sensitive approval UI. It lets the user choose which blocked popup URL to allow. If the chosen menu item does not map 1:1 to the reopened popup, then browser chrome is misrepresenting the security decision the user is making.

That exceeds a mere functional bug or display issue.

Example abuse scenario

One plausible setup:

  1. Hidden iframe A triggers one blocked popup
  2. iframe B triggers two blocked popups
  3. The UI shows iframe B's first popup as a benign-looking URL
  4. The user selects that menu item
  5. Because iframe B's entries are offset in the flattened list, the browser reopens iframe B's second popup instead

This lets an attacker exploit the user's trust in the browser's popup-selection UI.

Difference from prior cases

  • Older popup-blocker cases focused on wrong site attribution or wrong restoration context
  • This candidate is a modern implementation bug caused by flattening popup records across BrowsingContexts while replay still uses per-document local state

Duplicate check

  • Existing Exploit content only contained logi_homepage_pipe_split_js
  • No same-root-cause entry was found there
  • Family overlap exists with historical popup logic bugs, but not the same implementation cause

Safe reproduction

  1. Create a test page with multiple iframes in one tab
  2. From different frames, trigger multiple blocked window.open(...) attempts
  3. Open the popup-blocker menu
  4. Compare each displayed popup URI with the URI that actually reopens

Use only self-controlled test URLs.

Mitigation / fix direction

  • Store { browsingContext, innerWindowId, localPopupIndex } in each menu item instead of a flattened index
  • Or keep the flat list but explicitly preserve the local index for the source actor
  • Fix unblockAllPopups() to recompute local indices per browsing context instead of reusing flat indices

Bounty relevance

  • Logic bug, not memory corruption
  • Not a mere crash
  • Does not require prior content compromise
  • Arises during ordinary popup-blocker UI handling

This makes it a reasonable Mozilla client bug bounty report candidate as a browser-chrome security-decision confusion issue.

Chain Attack: Popup Confusion + Permission Request Escalation

The popup index confusion can be chained with permission requests for higher impact:

Attack Flow

  1. Attacker page embeds hidden iframes that trigger blocked popups
  2. iframe A triggers a popup to safe-page.html (benign URL shown in UI)
  3. iframe B triggers a popup to attacker-permissions.html (requests camera/mic/geolocation)
  4. User clicks the popup blocker and selects the "Safe Page" entry
  5. Due to index confusion, attacker-permissions.html opens instead
  6. Critical: The user's "Allow" click on the popup blocker provides user activation to the newly opened popup (via GetUserActivationModifiersForPopup() in nsGlobalWindowOuter.cpp:6874)
  7. The wrong popup immediately calls navigator.mediaDevices.getUserMedia() or Notification.requestPermission()
  8. Because the popup has valid user activation (mHasValidTransientUserGestureActivation = true), the permission request proceeds without "user gesture required" warnings
  9. A permission prompt appears for the attacker's origin

Why This Works

  • nsGlobalWindowOuter.cpp:6873-6914: User activation modifiers are retrieved from the browsing context before the popup opens
  • nsContentPermissionHelper.cpp:421-422: mHasValidTransientUserGestureActivation is captured from the document's current state at permission request time
  • The popup blocker "Allow" click → user gesture → inherited by wrong popup → enables immediate permission request

Impact Amplification

Without the chain: Phishing page opens (UI spoofing only)
With the chain: Permission prompt from wrong origin appears with user activation, potentially granting camera/mic/location to the attacker

Flags: sec-bounty?

The popup-blocker menu is a security-sensitive approval UI.

It's really more of an anti-abuse spam blocker, but if we're going to do it we shouldn't confuse our links. Assuming the user even wants to open a popup from a page that doesn't know how to do acceptable popups -- that's already suspicious behavior

Status: UNCONFIRMED → NEW
Component: Security → Popup Blocker
Ever confirmed: true
Product: Firefox → Toolkit

Maurice, you worked on some of this for redirect blocking, do you have a chance to take a look here?

Flags: needinfo?(mdauer)

Thanks for the report! It looks like this has been an issue ever since the popup blocker was introduced.

Assignee: nobody → mdauer
Status: NEW → ASSIGNED
Flags: needinfo?(mdauer)
Attached file (secure) —
Group: firefox-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 5 months ago
Resolution: --- → FIXED
Target Milestone: --- → 151 Branch
Flags: sec-bounty? → sec-bounty-
QA Whiteboard: [sec] [qa-triage-done-c152/b151]
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main151+][adv-esr115.36+][adv-esr140.11+]
Whiteboard: [client-bounty-form][adv-main151+][adv-esr115.36+][adv-esr140.11+] → [client-bounty-form][adv-main151+]
Alias: CVE-2026-8964
Flags: sec-bounty-hof+
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: