Medium CVSS 4.3 firefox Spoofing 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionAn attacker could cause a select dropdown to be shown over another tab; this could have led to user confusion and possible spoofing attacks.
ComponentLayout
Bug ClassSpoofing
Tracker1909535
Fix commitd0e24c06b1e5 (firefox) +12/-0
CWECWE-290
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedShaheen Fazim
Disclosed2024-11-26

Background

Combobox dropdown
the native popup list that appears when a user activates an HTML <select> element, rendered on the parent process by SelectParentHelper.
`browsingContext.isActive`
a flag indicating whether a given BrowsingContext (roughly, a tab’s document tree) is the one currently displayed to the user rather than a backgrounded tab.
Active tab
the foreground tab whose content is visible; content in non-active (background) tabs should not be able to paint UI over the visible tab.
`HTMLSelectEventListener`
the layout-side listener that handles mouse and key events on a <select> and decides when to open or update its dropdown.

Root Cause Analysis

When a <select> element received a mouse-down event, HTMLSelectEventListener::MouseDown proceeded to open or manipulate the combobox dropdown without ever checking whether the owning document belonged to the currently active tab. Similarly, SelectParentHelper.open in the parent process would unconditionally unhide the menulist popup and wire up its actor even when the requesting browser was in a background BrowsingContext or when the owning document did not hold focus. The violated invariant is that UI chrome such as a select dropdown must only be presented for content in the foreground, focused tab; a backgrounded document was able to trigger a dropdown that painted over whatever tab the user was actually viewing.

The fix restores the invariant on both sides: layout bails out early via IsInActiveTab before starting selection, and the parent helper checks browser.browsingContext.isActive and ownerDocument.hasFocus() and dismisses the dropdown instead of showing it when either fails.

Key insight
The core mistake was opening a <select> dropdown in response to content-driven events without confirming the content lives in the active, focused tab, allowing a background tab to spoof UI over the foreground one. The fix adds active-tab/focus guards in both HTMLSelectEventListener::MouseDown and SelectParentHelper.open so a non-active or unfocused context is short-circuited (returning early or sending Forms:DismissedDropDown) rather than rendering the popup.

Attack Path

  1. Prepare a malicious page An attacker crafts a page containing a <select> element and scripting that will attempt to open its dropdown programmatically or via synthesized interaction.
  2. Move the page to the background The page is loaded in a tab that is then backgrounded (for example via a pop-under or by the user switching to another tab), so its BrowsingContext is no longer active.
  3. Trigger the dropdown The background document initiates the mouse-down path so that HTMLSelectEventListener::MouseDown runs and SelectParentHelper.open is asked to display the combobox popup.
  4. Popup paints over the active tab Because no active-tab or focus check existed, the menulist is unhidden and drawn over the tab the user is currently viewing, appearing to belong to the foreground content.
  5. Spoof the user The attacker positions and styles the dropdown to impersonate trusted UI, inducing user confusion or a spoofing interaction against the wrong origin.

Impact Assessment

An attacker gains the ability to render a select dropdown over a different, currently-visible tab from a background BrowsingContext, enabling UI spoofing and user confusion in the browser’s chrome/content boundary. This is a moderate-severity logic error requiring that the attacker’s page be loaded (typically in a background or unfocused tab) and able to trigger the <select> open path; no memory corruption or code execution results, only visual/UX deception.

Changed Functions

FunctionChangeNotes
open
toolkit/actors/SelectParent.sys.mjs
modified

Files Changed

  • layout/forms/HTMLSelectEventListener.cpp
  • toolkit/actors/SelectParent.sys.mjs

Audit Directions

  • Content-triggered chrome UI
    Audit any parent-process or layout code that shows popups, panels, or overlays in response to content events for a missing active-tab or focus check before rendering.
  • `BrowsingContext.isActive` gaps
    Search for popup/menu open paths that manipulate a browser or BrowsingContext without consulting isActive, since background contexts should not paint over the foreground.
  • `hasFocus` omissions
    Review UI that assumes the owning document is focused (e.g. via ownerDocument.hasFocus()) and flag places that unhide or position user-facing elements without confirming focus.
diff --git a/layout/forms/HTMLSelectEventListener.cpp b/layout/forms/HTMLSelectEventListener.cpp
index 10ed410ee0e..e593b465677 100644
--- a/layout/forms/HTMLSelectEventListener.cpp
+++ b/layout/forms/HTMLSelectEventListener.cpp
@@ -403,6 +403,10 @@ nsresult HTMLSelectEventListener::MouseDown(dom::Event* aMouseEvent) {
     return NS_OK;
   }
 
+  if (!IsInActiveTab(mElement->OwnerDoc())) {
+    return NS_OK;
+  }
+
   // only allow selection with the left button
   // if a right button click is on the combobox itself
   // or on the select when in listbox mode, then let the click through
diff --git a/toolkit/actors/SelectParent.sys.mjs b/toolkit/actors/SelectParent.sys.mjs
index f92c6e81353..1eaf4f3b464 100644
--- a/toolkit/actors/SelectParent.sys.mjs
+++ b/toolkit/actors/SelectParent.sys.mjs
@@ -281,6 +281,14 @@ export var SelectParentHelper = {
   },
 
   open(browser, menulist, rect, isOpenedViaTouch, selectParentActor) {
+    if (
+      (browser && !browser.browsingContext.isActive) ||
+      !menulist.ownerDocument.hasFocus()
+    ) {
+      selectParentActor.sendAsyncMessage("Forms:DismissedDropDown", {});
+      return;
+    }
+
     this._actor = selectParentActor;
     menulist.hidden = false;
     this._currentBrowser = browser;
Loading diff…