Skip to content

A page (or any iframe in it) can register a fake player under the host page and switch FastStream off there #225

Closed

Description

@Nawid3333

Severity: medium

Where: chrome/player/main.mjs (PLAYER_LOADED carries parentFrameId: parseInt(urlParams.get('parent_frame_id')) from the player's own query string), chrome/background/background.mjs (PLAYER_LOADED handler: if (!frame.parent && msg.parentFrameId !== undefined) { const parentFrame = tab.getFrameOrCreate(msg.parentFrameId); frame.setParentFrame(parentFrame); }; onHeadersReceived: if (frame.hasPlayer()) return;; openPlayer: if (frame.playerOpening || frame.hasPlayer()) return;), chrome/background/TabTracker.mjs (isPlayerOfGoneDocument(playerFrame, parentFrameId, opener) { if (!opener) return false; ...)

What happens

player/index.html is web-accessible to <all_urls>. An iframe of any origin inside a page (an ad slot) that knows the profile's extension UUID (CLAUDE.md notes a page reads it from a player iframe's src; it is the same on every site) embeds moz-extension://<uuid>/player/index.html?parent_frame_id=0 with no opener. The player's own document load is a moz-extension: request, which the <all_urls> webRequest listener never sees, so frame.parent is unset when PLAYER_LOADED arrives and the page-chosen parentFrameId is taken. With opener absent, isPlayerOfGoneDocument returns false ("proves nothing and the player counts").

Result: the fake player becomes a child of frame 0 with isPlayer = true, so frame0.hasPlayer() is true: onHeadersReceived drops every stream the host page requests and openPlayer refuses frame 0 until the next navigation, exactly the failure CLAUDE.md describes for the PLAYER_LOADED race, now triggerable on purpose. Side effects: tab.playerCount++, tabHasPlayer() answers true so the toolbar's MPV/Off paths reload the tab, REQUEST_SOURCES hands the host page's detected streams (with their stored request headers) to the player inside the attacker's iframe, and the IS_FULL round trip calls OverlayGuard.guard() on the attacker's iframe in the host page. parseInt('x') also yields a NaN frame key. No path was found by which the attacker frame can read the sources (the player only ever posts the one-time link key to its parent), so this is denial of service plus unwanted DOM edits of the host page, not exfiltration.

Suggested fix

Do not take msg.parentFrameId on trust. Make opener mandatory for an extension player (content.js always sets it) and refuse a PLAYER_LOADED without one; when the named parent frame has no documentKey (background restarted), ask that frame's content script whether its DocumentKey equals the opener before adopting the parent. Or: openPlayer already records frame.playerOpeningAttempt; put the attempt number in the player URL from content.js and accept parentFrameId only when that parent frame has playerOpening set with the matching attempt (otherwise a standalone player with no parent, and no playerCount++). Reject a non-integer parentFrameId. Alternatively add the webNavigation permission and verify the parent with chrome.webNavigation.getFrame.

Confidence: medium-high (reasoned from the code paths; not executed against a live profile).


From a line-by-line code review of the tree at 41363fd (2026-10-02). Each claim above was checked against the source before filing; the reproduction steps and line references are as of that commit.


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    code-reviewFound by the 2026-10-02 line-by-line reviewsecuritySecurity finding

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions