Low CVSS 5.4 firefox Logic Error 🔧 Commit mapped

Overview

Low
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionA clickjacking vulnerability could have been used to trick a user into leaking saved payment card details to a malicious page.
ComponentToolkit
Bug ClassLogic Error
Tracker1954137
Fix commit1f1a1333169d (firefox) +14/-4
CWECWE-1021
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N
CISA KEVNot listed
CreditedAmeen Basha M K
Disclosed2025-05-27

Background

AutoComplete popup
the dropdown Firefox shows for form autofill, including saved payment-card details. The parent-process actor is AutoCompleteParent (toolkit/actors/AutoCompleteParent.sys.mjs).
Anti-clickjacking delay
to defend against UI-redressing, autofill menu items are temporarily disabled for a short popupDelay after the popup appears, so a click delivered immediately (e.g. because the attacker lured the pointer there) cannot instantly commit a fill.
Clickjacking (UI redressing)
an attacker overlays/times UI so a victim’s click lands on a sensitive control (here, an autofill entry) without the victim realizing it, causing saved data (payment-card details) to be filled and leaked to the attacker page.
`setTimeout`/`clearTimeout`
Gecko timer primitives from Timer.sys.mjs; the popup re-enables items after popupDelay via a timer.

Root Cause Analysis

The original code disabled all popup items, then scheduled a single one-shot re-enable: lazy.setTimeout(() => items.forEach(item => item.disabled = false), popupDelay). The flaw is that the delay was anchored only to popup creation and never reset on interaction. An attacker who could cause the popup to appear and then deliver a click just after popupDelay elapsed — or who kept the popup visible while timing a click to land right as items re-enabled — could get a click accepted against a freshly re-enabled item, committing an autofill of sensitive data (saved payment card) into an attacker-influenced form. In other words, a single fixed timer did not account for clicks arriving around the re-enable boundary, so the protective window could be defeated by well-timed input.

The patch converts the one-shot into a resettable, click-driven debounce. It imports clearTimeout, and wraps the re-enable in a delay() closure that, on each invocation, clears any pending timer (lazy.clearTimeout(timerId)) and restarts it (timerId = lazy.setTimeout(...)). Crucially it registers this.openedPopup.addEventListener("click", delay) so that every click on the popup restarts the disable window, and the final re-enable handler also removes the click listener. The result: a click near the boundary pushes the re-enable further out instead of being accepted, so an attacker can no longer time a click to slip through as items become enabled. This is a timing/logic error in the anti-clickjacking guard (CWE-1021, improper restriction of rendered UI), fixed by making the guard reset on interaction.

Key insight
The autofill re-enable timer was a single fixed-delay one-shot anchored to popup creation and never reset on clicks, so a click timed around the re-enable boundary could commit a fill; the fix restarts the disable window on every popup click (a debounce) so mistimed/malicious clicks can’t slip through.

Attack Path

  1. Attacker page induces the autofill popup (e.g. focuses a payment field) and positions a decoy so the victim is likely to click where the popup/item will be.
  2. Popup appears items are disabled for popupDelay.
  3. Timed click the attacker lures a victim click to arrive at/after the re-enable boundary (pre-fix the single timer had already re-enabled the items). (The precise luring technique is inferred; the diff proves only the timing/reset weakness.)
  4. Fill commits the click lands on a now-enabled saved-payment-card entry, autofilling it.
  5. Primitive the attacker page reads the filled payment-card details from its own form fields (confidentiality/integrity leak of saved card data; C:L/I:L).

Impact Assessment

This is a UI-redressing (clickjacking) defense weakness in the parent-process autofill actor, exploitable by a web attacker but requiring user interaction (a victim click; UI:R) and precise timing. The gain is leakage of saved payment-card details to the attacker page; no code execution or memory safety is involved. CVSS 5.4 reflects the interaction requirement and limited confidentiality/integrity impact. The fix strengthens a defense-in-depth timing guard rather than closing a memory sink.

Changed Functions

FunctionChangeNotes
if
toolkit/actors/AutoCompleteParent.sys.mjs
modified
notifyListeners
toolkit/actors/AutoCompleteParent.sys.mjs
modified

Files Changed

  • toolkit/actors/AutoCompleteParent.sys.mjs

Audit Directions

  • Other autofill/doorhanger UIs
    review login-manager, address, and credit-card doorhangers and permission prompts for the same “disable then fixed-delay re-enable” pattern without interaction-based reset.
  • Timer-anchored security delays
    audit anywhere a setTimeout-based grace period gates a sensitive action (download prompts, install prompts) to confirm the delay resets on user input near the boundary.
  • Event-listener lifecycle
    verify the new click listener is always removed (popup dismissal without the final re-enable) to avoid leaks or stale handlers that could themselves be abused.
diff --git a/toolkit/actors/AutoCompleteParent.sys.mjs b/toolkit/actors/AutoCompleteParent.sys.mjs
index 4dd0292d364..dc0b634e040 100644
--- a/toolkit/actors/AutoCompleteParent.sys.mjs
+++ b/toolkit/actors/AutoCompleteParent.sys.mjs
@@ -8,6 +8,7 @@ const lazy = {};
 
 ChromeUtils.defineESModuleGetters(lazy, {
   GeckoViewAutocomplete: "resource://gre/modules/GeckoViewAutocomplete.sys.mjs",
+  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
   setTimeout: "resource://gre/modules/Timer.sys.mjs",
 });
 
@@ -471,10 +472,19 @@ export class AutoCompleteParent extends JSWindowActorParent {
     );
     items.forEach(item => (item.disabled = true));
 
-    lazy.setTimeout(
-      () => items.forEach(item => (item.disabled = false)),
-      popupDelay
-    );
+    let timerId;
+    const delay = () => {
+      if (timerId) {
+        lazy.clearTimeout(timerId);
+      }
+      timerId = lazy.setTimeout(() => {
+        items.forEach(item => (item.disabled = false));
+        this.openedPopup?.removeEventListener("click", delay);
+      }, popupDelay);
+    };
+
+    this.openedPopup.addEventListener("click", delay);
+    delay();
   }
 
   notifyListeners() {
Loading diff…