From 44335e4a1d941961eb91d11a67ab0acd458708df Mon Sep 17 00:00:00 2001 From: Emma Zuehlcke Date: Wed, 12 Feb 2025 15:30:13 +0000 Subject: [PATCH] Bug 1866661, a=dmeehan Differential Revision: https://phabricator.services.mozilla.com/D237567 --- browser/base/content/browser.js | 6 +- .../tabbrowser/content/tabbrowser.js | 8 +- toolkit/content/widgets/notificationbox.js | 126 +++++++++++++++++- 3 files changed, 132 insertions(+), 8 deletions(-) diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 3b2b6e6cbc50..d77e0fd6432c 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -403,13 +403,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); const tabNotifications = document.getElementById("tab-notification-deck"); gNavToolbox.insertBefore(element, tabNotifications); - }); + }, securityDelayMS); }); ChromeUtils.defineLazyGetter(this, "InlineSpellCheckerUI", () => { diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js index 2674dc2bebf4..d8a4676844d3 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js @@ -122,6 +122,12 @@ "browser.tabs.allow_transparent_browser", false ); + XPCOMUtils.defineLazyPreferenceGetter( + this, + "_notificationEnableDelay", + "security.notification_enable_delay", + 500 + ); if (AppConstants.MOZ_CRASHREPORTER) { ChromeUtils.defineESModuleGetters(this, { @@ -970,7 +976,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 fc3e553ca424..524797341a5b 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; } @@ -141,10 +143,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. * * @return The 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 @@ -244,6 +254,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"; @@ -639,6 +656,13 @@ this.dismissable = true; this._shown = false; + // 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); } @@ -661,6 +685,8 @@ if (this.eventCallback) { this.eventCallback("disconnected"); } + // Clean up clickjacking listeners if active. + this._uninitClickJackingProtection(); } closeButtonTemplate() { @@ -715,6 +741,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; } @@ -765,7 +809,7 @@ } setButtons(buttons) { - this._buttons = buttons; + this._buttons = []; for (let button of buttons) { let link = button.link || button.supportPage; let localeId = button["l10n-id"]; @@ -814,7 +858,9 @@ } else { this.buttonContainer.appendChild(buttonElem); } + buttonElem.buttonInfo = button; + this._buttons.push(buttonElem); } } @@ -826,7 +872,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); }