CVE-2025-5267
Overview
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
disabledfor a shortpopupDelayafter 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 afterpopupDelayvia 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.
Attack Path
- 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.
- Popup appears
items are disabled for
popupDelay. - 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.)
- Fill commits the click lands on a now-enabled saved-payment-card entry, autofilling it.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
iftoolkit/actors/AutoCompleteParent.sys.mjs |
modified | |
notifyListenerstoolkit/actors/AutoCompleteParent.sys.mjs |
modified |
Files Changed
toolkit/actors/AutoCompleteParent.sys.mjs
Audit Directions
- Other autofill/doorhanger UIsreview 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 delaysaudit 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 lifecycleverify the new
clicklistener is always removed (popup dismissal without the final re-enable) to avoid leaks or stale handlers that could themselves be abused.
Patch
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() {