Low firefox Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionThe exception page for the HTTPS-Only feature, displayed when a website is opened via HTTP, lacked an anti-clickjacking delay, potentially allowing an attacker to trick a user into granting an exception and loading a webpage over HTTP.
ComponentDOM
Bug ClassLogic Error
Tracker1955182
Fix commitc8bf04636b0a (firefox) +129/-37
CISA KEVNot listed
Creditedhafiizh & kang ali
Disclosed2025-06-24

Changed Functions

FunctionChangeNotes
if
browser/base/content/browser-siteIdentity.js
modified
if
dom/security/test/https-first/browser_https_telemetry.js
modified
for
dom/security/test/https-only/browser_continue_button_delay.js
modified
add_task
dom/security/test/https-only/browser_continue_button_delay.js
modified

Files Changed

  • browser/base/content/browser-siteIdentity.js
  • dom/security/test/https-first/browser_https_telemetry.js
  • dom/security/test/https-only/browser_continue_button_delay.js
  • toolkit/components/httpsonlyerror/content/errorpage.html
  • toolkit/components/httpsonlyerror/content/errorpage.js
  • toolkit/components/httpsonlyerror/tests/browser/head.js
  • toolkit/themes/shared/aboutHttpsOnlyError.css
diff --git a/browser/base/content/browser-siteIdentity.js b/browser/base/content/browser-siteIdentity.js
index 71d085cc45c..def0375826a 100644
--- a/browser/base/content/browser-siteIdentity.js
+++ b/browser/base/content/browser-siteIdentity.js
@@ -617,6 +617,9 @@ var gIdentityHandler = {
       if (this._popupInitialized) {
         PanelMultiView.hidePopup(this._identityPopup);
       }
+      // Ensure the browser is focused again, otherwise we may not trigger the
+      // security delay on a potential error page following this reload.
+      gBrowser.selectedBrowser.focus();
       return;
     }
     // Otherwise we just refresh the interface
diff --git a/dom/security/test/https-first/browser_https_telemetry.js b/dom/security/test/https-first/browser_https_telemetry.js
index d952de1c94e..34242590e9e 100644
--- a/dom/security/test/https-first/browser_https_telemetry.js
+++ b/dom/security/test/https-first/browser_https_telemetry.js
@@ -375,7 +375,7 @@ add_task(async function () {
       );
       info("Waiting for openInsecureButton to be enabled.");
       function callback() {
-        if (!openInsecureButton.inert) {
+        if (!openInsecureButton.classList.contains("disabled")) {
           observer.disconnect();
           content.requestAnimationFrame(() => {
             content.requestAnimationFrame(() => {
@@ -385,7 +385,7 @@ add_task(async function () {
         }
       }
       const observer = new content.MutationObserver(callback);
-      observer.observe(openInsecureButton, { attributeFilter: ["inert"] });
+      observer.observe(openInsecureButton, { attributeFilter: ["class"] });
       callback();
     });
 
diff --git a/dom/security/test/https-only/browser_continue_button_delay.js b/dom/security/test/https-only/browser_continue_button_delay.js
index 6bdee1610e0..a307687bde1 100644
--- a/dom/security/test/https-only/browser_continue_button_delay.js
+++ b/dom/security/test/https-only/browser_continue_button_delay.js
@@ -1,5 +1,21 @@
 "use strict";
 
+// This test ensures the security delay (security.dialog_enable_delay) gets
+// properly applied to the "Continue" button on the HTTPS-Only error page. It
+// consists of the following checks:
+// 1. Check that the button gets enabled at the right time after a new tab is
+//    loaded
+// 2. Check that the button gets disabled and re-enabled with the correct
+//    timings on a focus loss due to a new tab being opened.
+// 3. Check that the button gets enabled with the correct timings when the
+//    HTTPS-Only error page is loaded through the identity pane.
+
+// We specifically want a insecure url here that will fail to upgrade
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const TEST_URL = "http://untrusted.example.com";
+const TEST_PRINCIPAL =
+  Services.scriptSecurityManager.createContentPrincipalFromOrigin(TEST_URL);
+
 function waitForEnabledButton() {
   return new Promise(resolve => {
     const button = content.document.getElementById("openInsecure");
@@ -7,43 +23,28 @@ function waitForEnabledButton() {
       for (const mutation of mutations) {
         if (
           mutation.type === "attributes" &&
-          mutation.attributeName === "inert" &&
-          !mutation.target.inert
+          mutation.attributeName === "class" &&
+          !mutation.target.classList.contains("disabled")
         ) {
           resolve();
         }
       }
     });
-    observer.observe(button, { attributeFilter: ["inert"] });
+    observer.observe(button, { attributeFilter: ["class"] });
     ok(
-      button.inert,
-      "The 'Continue to HTTP Site' button should be inert right after the error page is loaded."
+      button.classList.contains("disabled"),
+      "The 'Continue to HTTP Site' button should be disabled right after the error page is loaded/focused."
     );
   });
 }
 
-add_task(async function () {
-  waitForExplicitFinish();
-
-  await SpecialPowers.pushPrefEnv({
-    set: [["dom.security.https_only_mode", true]],
-  });
-
-  const specifiedDelay = Services.prefs.getIntPref(
-    "security.dialog_enable_delay",
-    1000
-  );
+const specifiedDelay = Services.prefs.getIntPref(
+  "security.dialog_enable_delay",
+  1000
+);
 
-  let loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
-  info("Loading insecure page");
+async function waitForEnabledButtonAndCheckTiming() {
   const startTime = Date.now();
-  BrowserTestUtils.startLoadingURIString(
-    gBrowser,
-    // We specifically want a insecure url here that will fail to upgrade
-    // eslint-disable-next-line @microsoft/sdl/no-insecure-url
-    "http://untrusted.example.com:80"
-  );
-  await loaded;
   await SpecialPowers.spawn(gBrowser.selectedBrowser, [], waitForEnabledButton);
   const endTime = Date.now();
 
@@ -54,6 +55,49 @@ add_task(async function () {
     specifiedDelay - 100,
     `The observed delay (${observedDelay}ms) should be roughly the same or greater than the delay specified in "security.dialog_enable_delay" (${specifiedDelay}ms)`
   );
+}
 
-  finish();
+add_task(async function () {
+  await SpecialPowers.pushPrefEnv({
+    set: [["dom.security.https_only_mode", true]],
+  });
+
+  info("Loading insecure page");
+  let loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+  BrowserTestUtils.startLoadingURIString(gBrowser, TEST_URL);
+  await loaded;
+  await waitForEnabledButtonAndCheckTiming();
+
+  info("Opening and closing a new tab");
+  let newTab = await BrowserTestUtils.openNewForegroundTab({
+    gBrowser,
+  });
+  await BrowserTestUtils.removeTab(newTab);
+  await waitForEnabledButtonAndCheckTiming();
+
+  info("Loading page with exception");
+  await Services.perms.addFromPrincipal(
+    TEST_PRINCIPAL,
+    "https-only-load-insecure",
+    Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION
+  );
+  loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+  BrowserTestUtils.startLoadingURIString(gBrowser, TEST_URL);
+  await loaded;
+
+  info("Opening identity pane");
+  document.getElementById("identity-icon-box").click();
+  const identityPopup = document.getElementById("identity-popup");
+  ok(!!identityPopup, "Identity pane should exist");
+  await BrowserTestUtils.waitForPopupEvent(identityPopup, "shown");
+
+  info("Removing exception in identity pane");
+  const menulist = document.getElementById(
+    "identity-popup-security-httpsonlymode-menulist"
+  );
+  ok(!!menulist, "Identity pane should contain HTTPS-Only menulist");
+  loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+  menulist.getItemAtIndex(0).doCommand();
+  await loaded;
+  await waitForEnabledButtonAndCheckTiming();
 });
diff --git a/toolkit/components/httpsonlyerror/content/errorpage.html b/toolkit/components/httpsonlyerror/content/errorpage.html
index 89e5e3cda37..1cfd46594a0 100644
--- a/toolkit/components/httpsonlyerror/content/errorpage.html
+++ b/toolkit/components/httpsonlyerror/content/errorpage.html
@@ -64,10 +64,12 @@
           class="primary"
           data-l10n-id="about-httpsonly-button-go-back"
         ></button>
+        <!-- This button only looks "disabled", but still allows JS events to
+             propagate for the clickjacking protection. -->
         <button
           id="openInsecure"
           data-l10n-id="about-httpsonly-button-continue-to-site"
-          inert
+          class="disabled"
         ></button>
       </div>
 
diff --git a/toolkit/components/httpsonlyerror/content/errorpage.js b/toolkit/components/httpsonlyerror/content/errorpage.js
index 6b5fc931f48..7c7d5fd5dbd 100644
--- a/toolkit/components/httpsonlyerror/content/errorpage.js
+++ b/toolkit/components/httpsonlyerror/content/errorpage.js
@@ -7,6 +7,8 @@
 "use strict";
 
 const searchParams = new URLSearchParams(document.documentURI.split("?")[1]);
+const clickjackingDelay = RPMGetIntPref("security.dialog_enable_delay", 1000);
+let clickjackingTimeout;
 
 function initPage() {
   if (!searchParams.get("e")) {
@@ -49,10 +51,8 @@ function initPage() {
     .getElementById("goBack")
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/dom/security/test/https-first/browser_https_telemetry.js b/dom/security/test/https-first/browser_https_telemetry.js
index d952de1c94e..34242590e9e 100644
--- a/dom/security/test/https-first/browser_https_telemetry.js
+++ b/dom/security/test/https-first/browser_https_telemetry.js
@@ -375,7 +375,7 @@ add_task(async function () {
       );
       info("Waiting for openInsecureButton to be enabled.");
       function callback() {
-        if (!openInsecureButton.inert) {
+        if (!openInsecureButton.classList.contains("disabled")) {
           observer.disconnect();
           content.requestAnimationFrame(() => {
             content.requestAnimationFrame(() => {
@@ -385,7 +385,7 @@ add_task(async function () {
         }
       }
       const observer = new content.MutationObserver(callback);
-      observer.observe(openInsecureButton, { attributeFilter: ["inert"] });
+      observer.observe(openInsecureButton, { attributeFilter: ["class"] });
       callback();
     });
diff --git a/dom/security/test/https-only/browser_continue_button_delay.js b/dom/security/test/https-only/browser_continue_button_delay.js
index 6bdee1610e0..a307687bde1 100644
--- a/dom/security/test/https-only/browser_continue_button_delay.js
+++ b/dom/security/test/https-only/browser_continue_button_delay.js
@@ -1,5 +1,21 @@
 "use strict";
 
+// This test ensures the security delay (security.dialog_enable_delay) gets
+// properly applied to the "Continue" button on the HTTPS-Only error page. It
+// consists of the following checks:
+// 1. Check that the button gets enabled at the right time after a new tab is
+//    loaded
+// 2. Check that the button gets disabled and re-enabled with the correct
+//    timings on a focus loss due to a new tab being opened.
+// 3. Check that the button gets enabled with the correct timings when the
+//    HTTPS-Only error page is loaded through the identity pane.
+
+// We specifically want a insecure url here that will fail to upgrade
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const TEST_URL = "http://untrusted.example.com";
+const TEST_PRINCIPAL =
+  Services.scriptSecurityManager.createContentPrincipalFromOrigin(TEST_URL);
+
 function waitForEnabledButton() {
   return new Promise(resolve => {
     const button = content.document.getElementById("openInsecure");
@@ -7,43 +23,28 @@ function waitForEnabledButton() {
       for (const mutation of mutations) {
         if (
           mutation.type === "attributes" &&
-          mutation.attributeName === "inert" &&
-          !mutation.target.inert
+          mutation.attributeName === "class" &&
+          !mutation.target.classList.contains("disabled")
         ) {
           resolve();
         }
       }
     });
-    observer.observe(button, { attributeFilter: ["inert"] });
+    observer.observe(button, { attributeFilter: ["class"] });
     ok(
-      button.inert,
-      "The 'Continue to HTTP Site' button should be inert right after the error page is loaded."
+      button.classList.contains("disabled"),
+      "The 'Continue to HTTP Site' button should be disabled right after the error page is loaded/focused."
     );
   });
 }
 
-add_task(async function () {
-  waitForExplicitFinish();
-
-  await SpecialPowers.pushPrefEnv({
-    set: [["dom.security.https_only_mode", true]],
-  });
-
-  const specifiedDelay = Services.prefs.getIntPref(
-    "security.dialog_enable_delay",
-    1000
-  );
+const specifiedDelay = Services.prefs.getIntPref(
+  "security.dialog_enable_delay",
+  1000
+);
 
-  let loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
-  info("Loading insecure page");
+async function waitForEnabledButtonAndCheckTiming() {
   const startTime = Date.now();
-  BrowserTestUtils.startLoadingURIString(
-    gBrowser,
-    // We specifically want a insecure url here that will fail to upgrade
-    // eslint-disable-next-line @microsoft/sdl/no-insecure-url
-    "http://untrusted.example.com:80"
-  );
-  await loaded;
   await SpecialPowers.spawn(gBrowser.selectedBrowser, [], waitForEnabledButton);
   const endTime = Date.now();
 
@@ -54,6 +55,49 @@ add_task(async function () {
     specifiedDelay - 100,
     `The observed delay (${observedDelay}ms) should be roughly the same or greater than the delay specified in "security.dialog_enable_delay" (${specifiedDelay}ms)`
   );
+}
 
-  finish();
+add_task(async function () {
+  await SpecialPowers.pushPrefEnv({
+    set: [["dom.security.https_only_mode", true]],
+  });
+
+  info("Loading insecure page");
+  let loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+  BrowserTestUtils.startLoadingURIString(gBrowser, TEST_URL);
+  await loaded;
+  await waitForEnabledButtonAndCheckTiming();
+
+  info("Opening and closing a new tab");
+  let newTab = await BrowserTestUtils.openNewForegroundTab({
+    gBrowser,
+  });
+  await BrowserTestUtils.removeTab(newTab);
+  await waitForEnabledButtonAndCheckTiming();
+
+  info("Loading page with exception");
+  await Services.perms.addFromPrincipal(
+    TEST_PRINCIPAL,
+    "https-only-load-insecure",
+    Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION
+  );
+  loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+  BrowserTestUtils.startLoadingURIString(gBrowser, TEST_URL);
+  await loaded;
+
+  info("Opening identity pane");
+  document.getElementById("identity-icon-box").click();
+  const identityPopup = document.getElementById("identity-popup");
+  ok(!!identityPopup, "Identity pane should exist");
+  await BrowserTestUtils.waitForPopupEvent(identityPopup, "shown");
+
+  info("Removing exception in identity pane");
+  const menulist = document.getElementById(
+    "identity-popup-security-httpsonlymode-menulist"
+  );
+  ok(!!menulist, "Identity pane should contain HTTPS-Only menulist");
+  loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+  menulist.getItemAtIndex(0).doCommand();
+  await loaded;
+  await waitForEnabledButtonAndCheckTiming();
 });
diff --git a/toolkit/components/httpsonlyerror/tests/browser/head.js b/toolkit/components/httpsonlyerror/tests/browser/head.js
index 7efbfbe8b50..ffe3c79df9e 100644
--- a/toolkit/components/httpsonlyerror/tests/browser/head.js
+++ b/toolkit/components/httpsonlyerror/tests/browser/head.js
@@ -81,7 +81,7 @@ function waitForAndClickOpenInsecureButton(browser) {
     );
     info("Waiting for openInsecureButton to be enabled.");
     function callback() {
-      if (!openInsecureButton.inert) {
+      if (!openInsecureButton.classList.contains("disabled")) {
         info("openInsecureButton was enabled, waiting two frames.");
         observer.disconnect();
         content.requestAnimationFrame(() => {
@@ -93,7 +93,7 @@ function waitForAndClickOpenInsecureButton(browser) {
       }
     }
     const observer = new content.MutationObserver(callback);
-    observer.observe(openInsecureButton, { attributeFilter: ["inert"] });
+    observer.observe(openInsecureButton, { attributeFilter: ["class"] });
     callback();
   });
 }
Loading diff…