Low firefox Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionInformation disclosure in the Password Manager component
ComponentToolkit
Bug ClassLogic Error
Tracker2027572
Fix commit90865a09f546 (firefox) +69/-13
CISA KEVNot listed
CreditedAv0id
Disclosed2026-06-16

Changed Functions

FunctionChangeNotes
add_task
browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js
modified
if
toolkit/components/passwordmgr/LoginHelper.sys.mjs
modified

Files Changed

  • browser/components/aboutlogins/AboutLoginsParent.sys.mjs
  • browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js
  • toolkit/components/passwordmgr/LoginHelper.sys.mjs
  • toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs
  • toolkit/components/satchel/megalist/aggregator/datasources/LoginDataSource.sys.mjs
diff --git a/browser/components/aboutlogins/AboutLoginsParent.sys.mjs b/browser/components/aboutlogins/AboutLoginsParent.sys.mjs
index fd1f392ed41..4ac658cacc9 100644
--- a/browser/components/aboutlogins/AboutLoginsParent.sys.mjs
+++ b/browser/components/aboutlogins/AboutLoginsParent.sys.mjs
@@ -276,7 +276,6 @@ export class AboutLoginsParent extends JSWindowActorParent {
 
     let { isAuthorized, telemetryEvent } = await lazy.LoginHelper.requestReauth(
       this.browsingContext.embedderElement,
-      isOSAuthEnabled,
       AboutLogins._authExpirationTime,
       messageText.value,
       captionText.value,
@@ -398,7 +397,6 @@ export class AboutLoginsParent extends JSWindowActorParent {
     let reason = "export_logins";
     let { isAuthorized, telemetryEvent } = await lazy.LoginHelper.requestReauth(
       this.browsingContext.embedderElement,
-      true,
       null, // Prompt regardless of a recent prompt
       messageText.value,
       captionText.value,
diff --git a/browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js b/browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js
index 10e3ea794f0..07e027ffa92 100644
--- a/browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js
+++ b/browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js
@@ -234,6 +234,67 @@ add_task(async function test_os_auth_and_prp() {
   LoginTestUtils.primaryPassword.disable();
 });
 
+// When OS auth is disabled and a primary password exists but is already
+// unlocked, the primary password dialog should be shown.
+add_task(
+  async function test_prp_shown_when_os_auth_disabled_and_prp_unlocked() {
+    const osAuthWasEnabled = LoginHelper.getOSAuthEnabled();
+    LoginHelper.setOSAuthEnabled(false);
+
+    await BrowserTestUtils.openNewForegroundTab({
+      gBrowser,
+      url: "about:logins",
+    });
+
+    // Enable PrP after opening the tab so it doesn't prompt on load.
+    LoginTestUtils.primaryPassword.enable();
+    info("PrP has been enabled");
+
+    // First reveal: token is locked so PrP dialog must appear.
+    let mpDialogShown = forceAuthTimeoutAndWaitForMPDialog("authenticate");
+    await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+      let loginItem = content.document.querySelector("login-item");
+      loginItem.shadowRoot.querySelector(".reveal-password-checkbox").click();
+    });
+    await mpDialogShown;
+    info("PrP dialog shown and authenticated");
+
+    await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+      let revealCheckbox = content.document
+        .querySelector("login-item")
+        .shadowRoot.querySelector(".reveal-password-checkbox");
+      Assert.ok(revealCheckbox.checked, "password revealed after PrP auth");
+      revealCheckbox.click();
+      Assert.ok(!revealCheckbox.checked, "password hidden again");
+    });
+
+    // PrP is now unlocked and OS auth is disabled. The second reveal should
+    // prompt for PrP again.
+    mpDialogShown = forceAuthTimeoutAndWaitForMPDialog("authenticate");
+    await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+      let loginItem = content.document.querySelector("login-item");
+      info("Clicking reveal, expecting PrP dialog (OS auth is disabled)");
+      loginItem.shadowRoot.querySelector(".reveal-password-checkbox").click();
+    });
+    await mpDialogShown;
+    info("PrP dialog shown again as expected when OS auth is disabled");
+
+    await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+      let revealCheckbox = content.document
+        .querySelector("login-item")
+        .shadowRoot.querySelector(".reveal-password-checkbox");
+      Assert.ok(
+        revealCheckbox.checked,
+        "reveal checkbox should be checked after PrP auth"
+      );
+    });
+
+    BrowserTestUtils.removeTab(gBrowser.selectedTab);
+    LoginTestUtils.primaryPassword.disable();
+    LoginHelper.setOSAuthEnabled(osAuthWasEnabled);
+  }
+);
+
 add_task(async function test_osAuth_shown_on_copy_password() {
   if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
     Assert.ok(
diff --git a/toolkit/components/passwordmgr/LoginHelper.sys.mjs b/toolkit/components/passwordmgr/LoginHelper.sys.mjs
index 8542feb5d34..91d496b46ca 100644
--- a/toolkit/components/passwordmgr/LoginHelper.sys.mjs
+++ b/toolkit/components/passwordmgr/LoginHelper.sys.mjs
@@ -1639,7 +1639,6 @@ export const LoginHelper = {
    *
    * @param {Element} browser
    *        The <browser> that the prompt should be shown on
-   * @param OSReauthEnabled Boolean indicating if OS reauth should be tried
    * @param expirationTime Optional timestamp indicating next required re-authentication
    * @param messageText Formatted and localized string to be displayed when the OS auth dialog is used.
    * @param captionText Formatted and localized string to be displayed when the OS auth dialog is used.
@@ -1647,7 +1646,6 @@ export const LoginHelper = {
    */
   async requestReauth(
     browser,
-    OSReauthEnabled,
     expirationTime,
     messageText,
     captionText,
@@ -1676,12 +1674,16 @@ export const LoginHelper = {
       };
     }
 
+    let isOSAuthEnabled = this.getOSAuthEnabled();
+
     // Default to true if there is no primary password and OS reauth is not available
-    if (!token.hasPassword && !OSReauthEnabled) {
+    if (!token.hasPassword && !isOSAuthEnabled) {
       isAuthorized = true;
       telemetryEvent = {
         name: "reauthenticateOsAuth",
-        value: "success_disabled",
+        value: lazy.OSKeyStore.canReauth()
+          ? "success_disabled"
+          : "success_unsupported_platform",
       };
       return {
         isAuthorized,
@@ -1689,8 +1691,8 @@ export const LoginHelper = {
       };
     }
     // Use the OS auth dialog if there is no primary password
-    // or if primary password is already unlocked.
-    if ((!token.hasPassword || token.isLoggedIn()) && OSReauthEnabled) {
+    // or if primary password is already unlocked and os auth is enabled.
+    if (isOSAuthEnabled && (!token.hasPassword || token.isLoggedIn())) {
       let result;
       try {
         isAuthorized = await this.verifyUserOSAuth(
diff --git a/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs b/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs
index 07f233f480c..d4af6fc6588 100644
--- a/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs
+++ b/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs
@@ -222,12 +222,10 @@ export class MegalistViewModel {
       ReauthPrimaryPassword: "reauth_cpm",
     };
     const reason = reasonMap[command.id];
-    const osAuthForPw = lazy.LoginHelper.getOSAuthEnabled();
     const { isAuthorized } = await lazy.LoginHelper.requestReauth(
       lazy.BrowserWindowTracker.getTopWindow({
         allowFromInactiveWorkspace: true,
       }).gBrowser,
-      osAuthForPw,
       this.#authExpirationTime,
       command.OSAuthPromptMessage,
       command.OSAuthCaptionMessage,
diff --git a/toolkit/components/satchel/megalist/aggregator/datasources/LoginDataSource.sys.mjs b/toolkit/components/satchel/megalist/aggregator/datasources/LoginDataSource.sys.mjs
index bc1600fd838..cef10aaa586 100644
--- a/toolkit/components/satchel/megalist/aggregator/datasources/LoginDataSource.sys.mjs
+++ b/toolkit/components/satchel/megalist/aggregator/datasources/LoginDataSource.sys.mjs
@@ -561,12 +561,9 @@ export class LoginDataSource extends DataSourceBase {
       allowFromInactiveWorkspace: true,
     }).browsingContext;
 
-    const isOSAuthEnabled = LoginHelper.getOSAuthEnabled();
-
     const reason = "export_cpm";
     let { isAuthorized, telemetryEvent } = await LoginHelper.requestReauth(
       browsingContext,
-      isOSAuthEnabled,
       null, // Prompt regardless of a recent prompt
       this.#exportPasswordsStrings.OSReauthMessage,
       this.#exportPasswordsStrings.OSAuthDialogCaption,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js b/browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js
index 10e3ea794f0..07e027ffa92 100644
--- a/browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js
+++ b/browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js
@@ -234,6 +234,67 @@ add_task(async function test_os_auth_and_prp() {
   LoginTestUtils.primaryPassword.disable();
 });
 
+// When OS auth is disabled and a primary password exists but is already
+// unlocked, the primary password dialog should be shown.
+add_task(
+  async function test_prp_shown_when_os_auth_disabled_and_prp_unlocked() {
+    const osAuthWasEnabled = LoginHelper.getOSAuthEnabled();
+    LoginHelper.setOSAuthEnabled(false);
+
+    await BrowserTestUtils.openNewForegroundTab({
+      gBrowser,
+      url: "about:logins",
+    });
+
+    // Enable PrP after opening the tab so it doesn't prompt on load.
+    LoginTestUtils.primaryPassword.enable();
+    info("PrP has been enabled");
+
+    // First reveal: token is locked so PrP dialog must appear.
+    let mpDialogShown = forceAuthTimeoutAndWaitForMPDialog("authenticate");
+    await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+      let loginItem = content.document.querySelector("login-item");
+      loginItem.shadowRoot.querySelector(".reveal-password-checkbox").click();
+    });
+    await mpDialogShown;
+    info("PrP dialog shown and authenticated");
+
+    await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+      let revealCheckbox = content.document
+        .querySelector("login-item")
+        .shadowRoot.querySelector(".reveal-password-checkbox");
+      Assert.ok(revealCheckbox.checked, "password revealed after PrP auth");
+      revealCheckbox.click();
+      Assert.ok(!revealCheckbox.checked, "password hidden again");
+    });
+
+    // PrP is now unlocked and OS auth is disabled. The second reveal should
+    // prompt for PrP again.
+    mpDialogShown = forceAuthTimeoutAndWaitForMPDialog("authenticate");
+    await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+      let loginItem = content.document.querySelector("login-item");
+      info("Clicking reveal, expecting PrP dialog (OS auth is disabled)");
+      loginItem.shadowRoot.querySelector(".reveal-password-checkbox").click();
+    });
+    await mpDialogShown;
+    info("PrP dialog shown again as expected when OS auth is disabled");
+
+    await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+      let revealCheckbox = content.document
+        .querySelector("login-item")
+        .shadowRoot.querySelector(".reveal-password-checkbox");
+      Assert.ok(
+        revealCheckbox.checked,
+        "reveal checkbox should be checked after PrP auth"
+      );
+    });
+
+    BrowserTestUtils.removeTab(gBrowser.selectedTab);
+    LoginTestUtils.primaryPassword.disable();
+    LoginHelper.setOSAuthEnabled(osAuthWasEnabled);
+  }
+);
+
 add_task(async function test_osAuth_shown_on_copy_password() {
   if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
     Assert.ok(
Loading diff…