Low firefox Logic Error 🔧 Commit mapped

Overview

Low
Severity
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 ClassLogic Error
Tracker1866661
Fix commitc4bd8080a937 (firefox) +132/-8
CISA KEVNot listed
CreditedHafiizh
Disclosed2025-03-04

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

Files Changed

  • browser/base/content/browser.js
  • browser/components/tabbrowser/content/tabbrowser.js
  • toolkit/content/widgets/notificationbox.js
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();
+          }
Loading diff…