Low CVSS 4.3 firefox Spoofing 🔧 Commit mapped

Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionA web page could trick a user into setting that site as the default handler for a custom URL protocol.
ComponentToolkit
Bug ClassSpoofing
Tracker1866661
Fix commitc4bd8080a937 (firefox) +132/-8
CWECWE-79 (Cross-site scripting)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedHafiizh
Disclosed2025-03-04

Background

Notification box
The high-priority info bar shown at the top of a Firefox window (MozElements.NotificationBox / <notification-message> custom element in toolkit/content/widgets/notificationbox.js). It hosts action buttons and links.
Custom protocol handler prompt
One such notification asks whether to let a site register itself as the default handler for a custom URL protocol; confirming grants the site that privileged association.
Clickjacking / tapjacking security delay
A short interval during which a security-relevant button is disabled after it appears, so a user cannot be tricked into clicking a control that was just surfaced (e.g. under a spoofed/covered UI). Governed here by security.notification_enable_delay.
`appendNotification` / `_initClickJackingProtection`
The API that shows a notification and the new routine that disables its buttons for the delay window and re-arms the delay on focus or premature clicks.

Root Cause Analysis

Despite the advisory’s CWE-79 (cross-site scripting) label, the diff implements clickjacking protection for notification-bar buttons, not an XSS fix — I flag that discrepancy. Previously, notification-bar buttons (including the custom-protocol-handler confirmation) were clickable immediately on display, so a page could time the appearance of the bar and induce the user to click the confirming button before they registered what it was, tricking them into setting the site as the default handler for a custom protocol. The invariant added is “a security-relevant notification button must not be actionable until a protective delay has elapsed and the user is focused.”

The patch threads a securityDelayMS (from the security.notification_enable_delay pref, default 500 ms) into the NotificationBox constructor (browser.js, tabbrowser.js, notificationbox.js). In appendNotification, unless clickjacking protection is explicitly disabled per-notification (aDisableClickJackingDelay) and when _securityDelayMS > 0, it calls newitem._initClickJackingProtection(this._securityDelayMS). The <notification-message> element gains state (_clickjackingDelayActive, _delayTimer, _focusHandler, _buttons) and methods _initClickJackingProtection / _startClickJackingDelay / _disableAllButtons / _enableAllButtons / _uninitClickJackingProtection: on display the buttons are disabled and a timer re-enables them after the delay; a focus handler restarts the delay when the window refocuses; and handleEvent intercepts early click/command on a button/link while the delay is active, calling stopPropagation/preventDefault and restarting the delay so a too-early click can’t slip through. setButtons is also corrected to rebuild the tracked _buttons array (this._buttons = [] then push each created button element) so the enable/disable logic operates on the real button nodes. This is a UI-timing / anti-redress hardening, with no memory-safety sink.

Key insight
Notification-bar buttons — including the custom-protocol-handler confirmation — were actionable the instant they appeared, so a page could trick a user into clicking before reading; the fix disables the buttons for a security delay that re-arms on window focus or premature clicks.

Attack Path

  1. Attacker page Causes Firefox to surface the notification bar asking to register the site as the default handler for a custom URL protocol.
  2. Time the click Uses UI timing / a covering interaction so the user’s imminent click lands on the confirm button the moment it appears, before they can read it (inferred: exact lure not shown by the diff).
  3. Premature confirm Pre-fix, the click is honored immediately.
  4. Primitive The site is set as the default handler for the custom protocol without the user’s informed consent (UI-redress / clickjacking). Post-fix the early click is swallowed and the delay restarts.

Impact Assessment

This is a chrome-UI clickjacking issue requiring user interaction (CVSS 4.3, AV:N/UI:R/C:L, low per Mozilla). There is no memory-safety primitive; the attacker gains an unintended privileged association (default custom-protocol handler) by defeating the user’s ability to read the prompt before acting. Impact is bounded by what registering a protocol handler enables and by the user still having to be present and clicking. The advisory’s XSS/CWE-79 classification is not substantiated by the diff, which is purely a clickjacking-delay mechanism.

Changed Functions

FunctionChangeNotes
if
browser/components/tabbrowser/content/tabbrowser.js
modified
constructor
toolkit/content/widgets/notificationbox.js
modified
if
toolkit/content/widgets/notificationbox.js
modified
closeButtonTemplate
toolkit/content/widgets/notificationbox.js
modified
handleEvent
toolkit/content/widgets/notificationbox.js
modified
setButtons
toolkit/content/widgets/notificationbox.js
modified
for
toolkit/content/widgets/notificationbox.js
modified
_initClickJackingProtection
toolkit/content/widgets/notificationbox.js
modified
_uninitClickJackingProtection
toolkit/content/widgets/notificationbox.js
modified
_startClickJackingDelay
toolkit/content/widgets/notificationbox.js
modified
_disableAllButtons
toolkit/content/widgets/notificationbox.js
modified
_enableAllButtons
toolkit/content/widgets/notificationbox.js
modified

Files Changed

  • browser/base/content/browser.js
  • browser/components/tabbrowser/content/tabbrowser.js
  • toolkit/content/widgets/notificationbox.js

Audit Directions

  • Other security-relevant prompts
    Verify permission, install, and other confirmation bars that grant privileges route through this delay (or an equivalent) and don’t pass aDisableClickJackingDelay = true inappropriately.
  • Focus/visibility re-arming
    Confirm the focus re-arm covers occlusion/alert-tab scenarios; check whether visibility changes without focus events can leave buttons enabled early.
  • `_buttons` tracking
    Ensure all code paths that add/replace notification buttons keep _buttons in sync so disable/enable can’t miss a button.
  • Default delay pref
    Review callers that could set security.notification_enable_delay to 0, which disables the protection entirely.
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index e5d9c4d9363..16790739e83 100644
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -410,13 +410,17 @@ ChromeUtils.defineLazyGetter(this, "ReferrerInfo", () =>
 
 // High priority notification bars shown at the top of the window.
 ChromeUtils.defineLazyGetter(this, "gNotificationBox", () => {
+  let securityDelayMS = Services.prefs.getIntPref(
+    "security.notification_enable_delay"
+  );
+
   return new MozElements.NotificationBox(element => {
     element.classList.add("global-notificationbox");
     element.setAttribute("notificationside", "top");
     element.setAttribute("prepend-notifications", true);
     // We want this before the tab notifications.
     document.getElementById("notifications-toolbar").prepend(element);
-  });
+  }, securityDelayMS);
 });
 
 ChromeUtils.defineLazyGetter(this, "InlineSpellCheckerUI", () => {
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
index 8a8351ae4e1..e815b9bd5a6 100644
--- a/browser/components/tabbrowser/content/tabbrowser.js
+++ b/browser/components/tabbrowser/content/tabbrowser.js
@@ -156,6 +156,12 @@
         "browser.tabs.unloadTabInContextMenu",
         false
       );
+      XPCOMUtils.defineLazyPreferenceGetter(
+        this,
+        "_notificationEnableDelay",
+        "security.notification_enable_delay",
+        500
+      );
 
       if (AppConstants.MOZ_CRASHREPORTER) {
         ChromeUtils.defineESModuleGetters(this, {
@@ -908,7 +914,7 @@
           if (browser == this.selectedBrowser) {
             this._updateVisibleNotificationBox(browser);
           }
-        });
+        }, this._notificationEnableDelay);
       }
       return browser._notificationBox;
     }
diff --git a/toolkit/content/widgets/notificationbox.js b/toolkit/content/widgets/notificationbox.js
index 7da1344f04b..983c566aaae 100644
--- a/toolkit/content/widgets/notificationbox.js
+++ b/toolkit/content/widgets/notificationbox.js
@@ -12,12 +12,14 @@
      * Creates a new class to handle a notification box, but does not add any
      * elements to the DOM until a notification has to be displayed.
      *
-     * @param insertElementFn
-     *        Called with the "notification-stack" element as an argument when the
-     *        first notification has to be displayed.
+     * @param insertElementFn Called with the "notification-stack" element as an
+     *        argument when the first notification has to be displayed.
+     * @param {Number} securityDelayMS - Delay in milliseconds until buttons are enabled to
+     * protect against click- and tapjacking.
      */
-    constructor(insertElementFn) {
+    constructor(insertElementFn, securityDelayMS = 0) {
       this._insertElementFn = insertElementFn;
+      this._securityDelayMS = securityDelayMS;
       this._animating = false;
       this.currentNotification = null;
     }
@@ -127,10 +129,18 @@
      *            Defines a Custom Element name to use as the "is" value on
      *            button creation.
      *        }
+     *    aDisableClickJackingDelay
+     *        Optional boolean arg to disable clickjacking protections. By
+     *        default the security delay is enabled.
      *
      * @returns {Promise<Object>} The <notification-message> element that is shown.
      */
-    async appendNotification(aType, aNotification, aButtons) {
+    async appendNotification(
+      aType,
+      aNotification,
+      aButtons,
+      aDisableClickJackingDelay = false
+    ) {
       if (
         aNotification.priority < this.PRIORITY_SYSTEM ||
         aNotification.priority > this.PRIORITY_CRITICAL_HIGH
@@ -213,6 +223,13 @@
         newitem.setAttribute("type", "warning");
       }
 
+      // If clickjacking protection is not explicitly disabled, enable it.
+      // aDisableClickJackingDelay is per notification, this._securityDelayMS is
+      // global for the entire notification box.
+      if (!aDisableClickJackingDelay && this._securityDelayMS > 0) {
+        newitem._initClickJackingProtection(this._securityDelayMS);
+      }
+
       // Animate the notification.
       newitem.style.display = "block";
       newitem.style.position = "fixed";
@@ -381,6 +398,13 @@
         this.timeout = 0;
         this.dismissable = true;
 
+        // Variables used for security delay / clickjacking protection.
+        this._clickjackingDelayActive = false;
+        this._securityDelayMS = 0;
+        this._delayTimer = null;
+        this._focusHandler = null;
+        this._buttons = [];
+
         this.addEventListener("click", this);
         this.addEventListener("command", this);
       }
@@ -403,6 +427,8 @@
         if (this.eventCallback) {
           this.eventCallback("disconnected");
         }
+        // Clean up clickjacking listeners if active.
+        this._uninitClickJackingProtection();
       }
 
       closeButtonTemplate() {
@@ -439,6 +465,24 @@
       }
 
       handleEvent(e) {
+        // If clickjacking delay is active, prevent any "click"/"command" from
+        // going through. Also restart the delay if the user tries to click too early.
+        if (this._clickjackingDelayActive) {
+          // Only relevant if user clicked on the notification’s actual button/link area.
+          if (
+            e.type === "click" &&
+            (e.target.localName === "button" ||
+              e.target.classList.contains("text-link") ||
+              e.target.classList.contains("notification-link"))
+          ) {
+            // Stop immediate action, restart the delay
+            e.stopPropagation();
+            e.preventDefault();
+            this._startClickJackingDelay();
+            return;
+          }
+        }
+
         if (e.type == "click" && e.target.localName != "label") {
           return;
         }
@@ -487,7 +531,7 @@
       }
 
       setButtons(buttons) {
-        this._buttons = buttons;
+        this._buttons = [];
         for (let button of buttons) {
           let link = button.link || button.supportPage;
           let localeId = button["l10n-id"];
@@ -536,7 +580,9 @@
           } else {
             this.buttonContainer.appendChild(buttonElem);
           }
+
           buttonElem.buttonInfo = button;
+          this._buttons.push(buttonElem);
         }
       }
 
@@ -546,7 +592,75 @@
         }
         super.dismiss();
       }
+
+      /**
+       * Initialize clickjacking protection for this notification, disabling
+       * buttons initially and re-enabling them after a short delay. The delay
+       * restarts on window focus or if the user attempts to click during the
+       * disabled period.
+       *
+       * @param {Number} securityDelayMS - ClickJacking delay to apply
+       * (milliseconds).
+       */
+      _initClickJackingProtection(securityDelayMS) {
+        if (this._clickjackingDelayActive) {
+          return; // Already enabled.
+        }
+
+        this._securityDelayMS = securityDelayMS;
+        // Attach a global focus handler so we can restart the delay when the window
+        // refocuses (e.g., user navigated away or used a popup).
+        this._focusHandler = () => {
+          // If the notification is still connected, restart the delay.
+          if (this.isConnected) {
+            this._startClickJackingDelay();
+          }
+        };
+
+        window.addEventListener("focus", this._focusHandler, true);
+        this._startClickJackingDelay();
+      }
+
+      /**
+       * Remove any event listeners or timers related to clickjacking protection.
+       */
+      _uninitClickJackingProtection() {
+        window.removeEventListener("focus", this._focusHandler, true);
+        this._focusHandler = null;
+        if (this._delayTimer) {
+          clearTimeout(this._delayTimer);
+          this._delayTimer = null;
+        }
+        this._enableAllButtons();
+        this._clickjackingDelayActive = false;
+      }
+
+      _startClickJackingDelay() {
+        this._clickjackingDelayActive = true;
+        this._disableAllButtons();
+        if (this._delayTimer) {
+          clearTimeout(this._delayTimer);
+        }
+        this._delayTimer = setTimeout(() => {
+          this._clickjackingDelayActive = false;
+          this._enableAllButtons();
+          this._delayTimer = null;
+        }, this._securityDelayMS);
+      }
+
+      _disableAllButtons() {
+        for (let button of this._buttons) {
+          button.disabled = true;
+        }
+      }
+
+      _enableAllButtons() {
+        for (let button of this._buttons) {
+          button.disabled = false;
+        }
+      }
     }
+
     if (!customElements.get("notification-message")) {
       customElements.define("notification-message", NotificationMessage);
     }
Loading diff…