Low firefox Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionIf a user visited a webpage with an invalid TLS certificate, and granted an exception, the webpage was able to provide a WebAuthn challenge that the user would be prompted to complete. This is in violation of the WebAuthN spec which requires "a secure transport established without errors".
ComponentDOM
Bug ClassLogic Error
Tracker1954033
Fix commitdca3898ce43c (firefox) +126/-0
CISA KEVNot listed
CreditedSimon
Disclosed2025-06-24

Changed Functions

FunctionChangeNotes
switch
dom/webauthn/WebAuthnUtil.cpp
modified
if
dom/webauthn/tests/browser/browser_webauthn_cert_override.js
modified

Files Changed

  • dom/webauthn/WebAuthnTransactionParent.cpp
  • dom/webauthn/WebAuthnUtil.cpp
  • dom/webauthn/WebAuthnUtil.h
  • dom/webauthn/tests/browser/browser.toml
  • dom/webauthn/tests/browser/browser_webauthn_cert_override.js
diff --git a/dom/webauthn/WebAuthnTransactionParent.cpp b/dom/webauthn/WebAuthnTransactionParent.cpp
index 3c704b9620e..f83880bc825 100644
--- a/dom/webauthn/WebAuthnTransactionParent.cpp
+++ b/dom/webauthn/WebAuthnTransactionParent.cpp
@@ -121,6 +121,11 @@ mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestRegister(
   WindowGlobalParent* manager = static_cast<WindowGlobalParent*>(Manager());
   nsIPrincipal* principal = manager->DocumentPrincipal();
 
+  if (!IsWebAuthnAllowedForTransportSecurityInfo(manager->GetSecurityInfo())) {
+    aResolver(NS_ERROR_DOM_SECURITY_ERR);
+    return IPC_OK();
+  }
+
   if (!IsWebAuthnAllowedForPrincipal(principal)) {
     aResolver(NS_ERROR_DOM_SECURITY_ERR);
     return IPC_OK();
@@ -311,6 +316,11 @@ mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestSign(
   WindowGlobalParent* manager = static_cast<WindowGlobalParent*>(Manager());
   nsIPrincipal* principal = manager->DocumentPrincipal();
 
+  if (!IsWebAuthnAllowedForTransportSecurityInfo(manager->GetSecurityInfo())) {
+    aResolver(NS_ERROR_DOM_SECURITY_ERR);
+    return IPC_OK();
+  }
+
   if (!IsWebAuthnAllowedForPrincipal(principal)) {
     aResolver(NS_ERROR_DOM_SECURITY_ERR);
     return IPC_OK();
diff --git a/dom/webauthn/WebAuthnUtil.cpp b/dom/webauthn/WebAuthnUtil.cpp
index 5fb65ac33ce..96d20e6fbf3 100644
--- a/dom/webauthn/WebAuthnUtil.cpp
+++ b/dom/webauthn/WebAuthnUtil.cpp
@@ -132,6 +132,28 @@ bool IsWebAuthnAllowedForPrincipal(const nsCOMPtr<nsIPrincipal>& aPrincipal) {
   return true;
 }
 
+bool IsWebAuthnAllowedForTransportSecurityInfo(
+    nsITransportSecurityInfo* aSecurityInfo) {
+  nsITransportSecurityInfo::OverridableErrorCategory overridableErrorCategory;
+  if (!aSecurityInfo || NS_FAILED(aSecurityInfo->GetOverridableErrorCategory(
+                            &overridableErrorCategory))) {
+    return false;
+  }
+
+  switch (overridableErrorCategory) {
+    case nsITransportSecurityInfo::OverridableErrorCategory::ERROR_UNSET:
+      return true;
+    case nsITransportSecurityInfo::OverridableErrorCategory::ERROR_TIME:
+      return true;
+    case nsITransportSecurityInfo::OverridableErrorCategory::ERROR_TRUST:
+      return false;
+    case nsITransportSecurityInfo::OverridableErrorCategory::ERROR_DOMAIN:
+      return false;
+    default:
+      return false;
+  }
+}
+
 bool IsValidRpId(const nsCOMPtr<nsIPrincipal>& aPrincipal,
                  const nsACString& aRpId) {
   // This checks two of the conditions defined in
diff --git a/dom/webauthn/WebAuthnUtil.h b/dom/webauthn/WebAuthnUtil.h
index 68f50a2dcff..0806db47e06 100644
--- a/dom/webauthn/WebAuthnUtil.h
+++ b/dom/webauthn/WebAuthnUtil.h
@@ -19,6 +19,9 @@ bool IsWebAuthnAllowedInDocument(const nsCOMPtr<Document>& aDoc);
 
 bool IsWebAuthnAllowedForPrincipal(const nsCOMPtr<nsIPrincipal>& aPrincipal);
 
+bool IsWebAuthnAllowedForTransportSecurityInfo(
+    nsITransportSecurityInfo* aSecurityInfo);
+
 nsresult DefaultRpId(const nsCOMPtr<nsIPrincipal>& aPrincipal,
                      /* out */ nsACString& aRpId);
 
diff --git a/dom/webauthn/tests/browser/browser.toml b/dom/webauthn/tests/browser/browser.toml
index 9bca41566ef..0aaeb5eb3c4 100644
--- a/dom/webauthn/tests/browser/browser.toml
+++ b/dom/webauthn/tests/browser/browser.toml
@@ -21,6 +21,8 @@ run-if = ["os != 'win'"] # Test not relevant on 1903+
 ["browser_fido_appid_extension.js"]
 run-if = ["os != 'win'"] # Test not relevant on 1903+
 
+["browser_webauthn_cert_override.js"]
+
 ["browser_webauthn_conditional_mediation.js"]
 
 ["browser_webauthn_ipaddress.js"]
diff --git a/dom/webauthn/tests/browser/browser_webauthn_cert_override.js b/dom/webauthn/tests/browser/browser_webauthn_cert_override.js
new file mode 100644
index 00000000000..abe77d95523
--- /dev/null
+++ b/dom/webauthn/tests/browser/browser_webauthn_cert_override.js
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_virtual_authenticator();
+
+let expectSecurityError = expectError("Security");
+
+async function test_webauthn_with_cert_override(
+  aTestDomain,
+  aExpectSecurityError
+) {
+  let certOverrideService = Cc[
+    "@mozilla.org/security/certoverride;1"
+  ].getService(Ci.nsICertOverrideService);
+
+  let testURL = "https://" + aTestDomain;
+  let certErrorLoaded;
+  let tab = await BrowserTestUtils.openNewForegroundTab(
+    gBrowser,
+    () => {
+      gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, testURL);
+      let browser = gBrowser.selectedBrowser;
+      certErrorLoaded = BrowserTestUtils.waitForErrorPage(browser);
+    },
+    false
+  );
+  info("Waiting for cert error page.");
+  await certErrorLoaded;
+
+  let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+  info("Adding certificate error override.");
+  await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+    let doc = content.document;
+    let exceptionButton = doc.getElementById("exceptionDialogButton");
+    exceptionButton.click();
+  });
+
+  info("Waiting for page load.");
+  await loaded;
+
+  await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+    let doc = content.document;
+    ok(
+      !doc.documentURI.startsWith("about:certerror"),
+      "Exception has been added."
+    );
+  });
+
+  let makeCredPromise = promiseWebAuthnMakeCredential(tab, "none", "preferred");
+  if (aExpectSecurityError) {
+    await makeCredPromise.then(arrivingHereIsBad).catch(expectSecurityError);
+    ok(
+      true,
+      "Calling navigator.credentials.create() results in a security error"
+    );
+  } else {
+    await makeCredPromise.catch(arrivingHereIsBad);
+    ok(true, "Calling navigator.credentials.create() is allowed");
+  }
+
+  let getAssertionPromise = promiseWebAuthnGetAssertionDiscoverable(tab);
+  if (aExpectSecurityError) {
+    await getAssertionPromise
+      .then(arrivingHereIsBad)
+      .catch(expectSecurityError);
+    ok(true, "Calling navigator.credentials.get() results in a security error");
+  } else {
+    await getAssertionPromise.catch(arrivingHereIsBad);
+    ok(true, "Calling navigator.credentials.get() results in a security error");
+  }
+
+  certOverrideService.clearValidityOverride(aTestDomain, -1, {});
+
+  loaded = BrowserTestUtils.waitForErrorPage(tab.linkedBrowser);
+  BrowserCommands.reloadSkipCache();
+  await loaded;
+
+  BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
+add_task(() => test_webauthn_with_cert_override("expired.example.com", false));
+add_task(() => test_webauthn_with_cert_override("untrusted.example.com", true));
+add_task(() =>
+  test_webauthn_with_cert_override("no-subject-alt-name.example.com", true)
+);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/dom/webauthn/tests/browser/browser.toml b/dom/webauthn/tests/browser/browser.toml
index 9bca41566ef..0aaeb5eb3c4 100644
--- a/dom/webauthn/tests/browser/browser.toml
+++ b/dom/webauthn/tests/browser/browser.toml
@@ -21,6 +21,8 @@ run-if = ["os != 'win'"] # Test not relevant on 1903+
 ["browser_fido_appid_extension.js"]
 run-if = ["os != 'win'"] # Test not relevant on 1903+
 
+["browser_webauthn_cert_override.js"]
+
 ["browser_webauthn_conditional_mediation.js"]
 
 ["browser_webauthn_ipaddress.js"]
diff --git a/dom/webauthn/tests/browser/browser_webauthn_cert_override.js b/dom/webauthn/tests/browser/browser_webauthn_cert_override.js
new file mode 100644
index 00000000000..abe77d95523
--- /dev/null
+++ b/dom/webauthn/tests/browser/browser_webauthn_cert_override.js
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_virtual_authenticator();
+
+let expectSecurityError = expectError("Security");
+
+async function test_webauthn_with_cert_override(
+  aTestDomain,
+  aExpectSecurityError
+) {
+  let certOverrideService = Cc[
+    "@mozilla.org/security/certoverride;1"
+  ].getService(Ci.nsICertOverrideService);
+
+  let testURL = "https://" + aTestDomain;
+  let certErrorLoaded;
+  let tab = await BrowserTestUtils.openNewForegroundTab(
+    gBrowser,
+    () => {
+      gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, testURL);
+      let browser = gBrowser.selectedBrowser;
+      certErrorLoaded = BrowserTestUtils.waitForErrorPage(browser);
+    },
+    false
+  );
+  info("Waiting for cert error page.");
+  await certErrorLoaded;
+
+  let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+  info("Adding certificate error override.");
+  await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+    let doc = content.document;
+    let exceptionButton = doc.getElementById("exceptionDialogButton");
+    exceptionButton.click();
+  });
+
+  info("Waiting for page load.");
+  await loaded;
+
+  await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+    let doc = content.document;
+    ok(
+      !doc.documentURI.startsWith("about:certerror"),
+      "Exception has been added."
+    );
+  });
+
+  let makeCredPromise = promiseWebAuthnMakeCredential(tab, "none", "preferred");
+  if (aExpectSecurityError) {
+    await makeCredPromise.then(arrivingHereIsBad).catch(expectSecurityError);
+    ok(
+      true,
+      "Calling navigator.credentials.create() results in a security error"
+    );
+  } else {
+    await makeCredPromise.catch(arrivingHereIsBad);
+    ok(true, "Calling navigator.credentials.create() is allowed");
+  }
+
+  let getAssertionPromise = promiseWebAuthnGetAssertionDiscoverable(tab);
+  if (aExpectSecurityError) {
+    await getAssertionPromise
+      .then(arrivingHereIsBad)
+      .catch(expectSecurityError);
+    ok(true, "Calling navigator.credentials.get() results in a security error");
+  } else {
+    await getAssertionPromise.catch(arrivingHereIsBad);
+    ok(true, "Calling navigator.credentials.get() results in a security error");
+  }
+
+  certOverrideService.clearValidityOverride(aTestDomain, -1, {});
+
+  loaded = BrowserTestUtils.waitForErrorPage(tab.linkedBrowser);
+  BrowserCommands.reloadSkipCache();
+  await loaded;
+
+  BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
+add_task(() => test_webauthn_with_cert_override("expired.example.com", false));
+add_task(() => test_webauthn_with_cert_override("untrusted.example.com", true));
+add_task(() =>
+  test_webauthn_with_cert_override("no-subject-alt-name.example.com", true)
+);
Loading diff…