CVE-2025-1935
Overview
Background
- Notification box
- The high-priority info bar shown at the top of a Firefox window (
MozElements.NotificationBox/<notification-message>custom element intoolkit/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.
Attack Path
- Attacker page Causes Firefox to surface the notification bar asking to register the site as the default handler for a custom URL protocol.
- 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).
- Premature confirm Pre-fix, the click is honored immediately.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifbrowser/components/tabbrowser/content/tabbrowser.js |
modified | |
constructortoolkit/content/widgets/notificationbox.js |
modified | |
iftoolkit/content/widgets/notificationbox.js |
modified | |
closeButtonTemplatetoolkit/content/widgets/notificationbox.js |
modified | |
handleEventtoolkit/content/widgets/notificationbox.js |
modified | |
setButtonstoolkit/content/widgets/notificationbox.js |
modified | |
fortoolkit/content/widgets/notificationbox.js |
modified | |
_initClickJackingProtectiontoolkit/content/widgets/notificationbox.js |
modified | |
_uninitClickJackingProtectiontoolkit/content/widgets/notificationbox.js |
modified | |
_startClickJackingDelaytoolkit/content/widgets/notificationbox.js |
modified | |
_disableAllButtonstoolkit/content/widgets/notificationbox.js |
modified | |
_enableAllButtonstoolkit/content/widgets/notificationbox.js |
modified |
Files Changed
browser/base/content/browser.jsbrowser/components/tabbrowser/content/tabbrowser.jstoolkit/content/widgets/notificationbox.js
Audit Directions
- Other security-relevant promptsVerify permission, install, and other confirmation bars that grant privileges route through this delay (or an equivalent) and don’t pass
aDisableClickJackingDelay = trueinappropriately. - Focus/visibility re-armingConfirm the
focusre-arm covers occlusion/alert-tab scenarios; check whether visibility changes without focus events can leave buttons enabled early. - `_buttons` trackingEnsure all code paths that add/replace notification buttons keep
_buttonsin sync so disable/enable can’t miss a button. - Default delay prefReview callers that could set
security.notification_enable_delayto 0, which disables the protection entirely.
Patch
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);
}