Moderate firefox Logic Error 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionSite isolation issue in the DOM: Web Authentication component
ComponentDOM
Bug ClassLogic Error
Tracker2067571
Fix commit57fb9c11807b (firefox) +104/-1
CISA KEVNot listed
Creditedcl0und
Disclosed2026-10-13

Changed Functions

FunctionChangeNotes
add_task
toolkit/components/extensions/test/mochitest/test_ext_webauthn.html
modified
if
toolkit/components/extensions/test/mochitest/test_ext_webauthn.html
modified

Files Changed

  • dom/webauthn/WebAuthnUtil.cpp
  • toolkit/components/extensions/test/mochitest/test_ext_webauthn.html
diff --git a/dom/webauthn/WebAuthnUtil.cpp b/dom/webauthn/WebAuthnUtil.cpp
index 3b0a7f10a94..f4a4662ddd7 100644
--- a/dom/webauthn/WebAuthnUtil.cpp
+++ b/dom/webauthn/WebAuthnUtil.cpp
@@ -9,7 +9,10 @@
 #include "mozilla/Base64.h"
 #include "mozilla/BasePrincipal.h"
 #include "mozilla/Components.h"
+#include "mozilla/ExtensionPolicyService.h"
 #include "mozilla/StaticPrefs_security.h"
+#include "mozilla/dom/BrowsingContextGroup.h"
+#include "mozilla/dom/RemoteType.h"
 #include "mozilla/dom/WebAuthenticationBinding.h"
 #include "mozilla/dom/WindowGlobalParent.h"
 #include "mozilla/dom/nsMixedContentBlocker.h"
@@ -140,6 +143,25 @@ bool IsWebAuthnAllowedInContext(WindowGlobalParent* aContext) {
     return false;
   }
 
+  if (extensions::WebExtensionPolicy* policy =
+          BasePrincipal::Cast(principal)->AddonPolicy()) {
+    if (!policy->Active()) {
+      return false;
+    }
+    if (ContentParent* contentParent = aContext->GetContentParent()) {
+      RefPtr<BrowsingContextGroup> group = BrowsingContextGroup::GetExisting(
+          policy->GetBrowsingContextGroupId());
+      if (!group || group->GetHostProcess(RemoteType(
+                        RemoteType::Kind::Extension)) != contentParent) {
+        return false;
+      }
+    } else if (ExtensionPolicyService::GetSingleton().UseRemoteExtensions()) {
+      // A null ContentParent means that the document is in the parent process,
+      // which only hosts extension documents when extensions are not remote.
+      return false;
+    }
+  }
+
   if (principal->GetIsIpAddress()) {
     return false;
   }
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webauthn.html b/toolkit/components/extensions/test/mochitest/test_ext_webauthn.html
index 2dddb5cb165..f27539cab6a 100644
--- a/toolkit/components/extensions/test/mochitest/test_ext_webauthn.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webauthn.html
@@ -36,7 +36,7 @@ add_setup(async () => {
   });
 });
 
-function loadTestExtension(testScript) {
+function loadTestExtension(testScript, manifestExtras = {}) {
   return ExtensionTestUtils.loadExtension({
     manifest: {
       host_permissions: ["https://www.example.com/*"],
@@ -44,6 +44,7 @@ function loadTestExtension(testScript) {
       browser_specific_settings: {
         gecko: { id: "@webauthn-test" },
       },
+      ...manifestExtras,
     },
     files: {
       "test.html": `<!DOCTYPE html>
@@ -104,6 +105,27 @@ add_task(async function test_webauthn_extension_origin_rpid() {
   await extension.unload();
 });
 
+add_task(async function test_webauthn_rpid_in_web_content_process() {
+  let extension = loadTestExtension(framed_rpid_script, {
+    web_accessible_resources: ["test.html"],
+  });
+
+  await extension.startup();
+
+  let tab = await AppTestDelegate.openNewForegroundTab(
+    window,
+    `moz-extension://${extension.uuid}/test.html`
+  );
+
+  await extension.awaitMessage("framed-done");
+
+  extension.sendMessage("close-web-tab");
+  await extension.awaitMessage("web-tab-closed");
+
+  await AppTestDelegate.removeTab(window, tab);
+  await extension.unload();
+});
+
 add_task(async function test_webauthn_forbidden_rpid() {
   let extension = loadTestExtension(forbidden_rpid_script);
 
@@ -363,6 +385,65 @@ async function extension_origin_rpid_script() {
   browser.test.sendMessage("done");
 }
 
+async function framed_rpid_script() {
+  if (window.parent !== window) {
+    // This instance of test.html is framed by a web page, so it is hosted by
+    // the web page's process and cannot claim www.example.com as an RP ID.
+    //
+    // Note: Bug 2044384 may move extension frames to the extension process,
+    // which would make IsWebAuthnAllowedInContext accept this request and
+    // cause the test to fail.
+    let challenge = new Uint8Array(32);
+    crypto.getRandomValues(challenge);
+
+    await browser.test.assertRejects(
+      navigator.credentials.get({
+        publicKey: {
+          rpId: "www.example.com",
+          allowCredentials: [
+            {
+              type: "public-key",
+              id: crypto.getRandomValues(new Uint8Array(32)),
+            },
+          ],
+          challenge,
+        },
+      }),
+      /The operation is insecure/,
+      "get() from an extension frame in a web page throws SecurityError"
+    );
+
+    browser.test.sendMessage("framed-done");
+    return;
+  }
+
+  let tabReady = new Promise(resolve => {
+    browser.tabs.onUpdated.addListener((id, { status }, { url }) => {
+      if (status === "complete" && url === "https://www.example.com/") {
+        resolve();
+      }
+    });
+  });
+  const tab = await browser.tabs.create({ url: "https://www.example.com/" });
+  await tabReady;
+
+  browser.test.onMessage.addListener(async () => {
+    await browser.tabs.remove(tab.id);
+    browser.test.sendMessage("web-tab-closed");
+  });
+
+  await browser.scripting.executeScript({
+    target: { tabId: tab.id },
+    func: frameUrl => {
+      let frame = document.createElement("iframe");
+      frame.setAttribute("allow", "publickey-credentials-get");
+      frame.setAttribute("src", frameUrl);
+      document.body.appendChild(frame);
+    },
+    args: [browser.runtime.getURL("test.html")],
+  });
+}
+
 async function forbidden_rpid_script() {
   let challenge = new Uint8Array(32);
   crypto.getRandomValues(challenge);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webauthn.html b/toolkit/components/extensions/test/mochitest/test_ext_webauthn.html
index 2dddb5cb165..f27539cab6a 100644
--- a/toolkit/components/extensions/test/mochitest/test_ext_webauthn.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webauthn.html
@@ -36,7 +36,7 @@ add_setup(async () => {
   });
 });
 
-function loadTestExtension(testScript) {
+function loadTestExtension(testScript, manifestExtras = {}) {
   return ExtensionTestUtils.loadExtension({
     manifest: {
       host_permissions: ["https://www.example.com/*"],
@@ -44,6 +44,7 @@ function loadTestExtension(testScript) {
       browser_specific_settings: {
         gecko: { id: "@webauthn-test" },
       },
+      ...manifestExtras,
     },
     files: {
       "test.html": `<!DOCTYPE html>
@@ -104,6 +105,27 @@ add_task(async function test_webauthn_extension_origin_rpid() {
   await extension.unload();
 });
 
+add_task(async function test_webauthn_rpid_in_web_content_process() {
+  let extension = loadTestExtension(framed_rpid_script, {
+    web_accessible_resources: ["test.html"],
+  });
+
+  await extension.startup();
+
+  let tab = await AppTestDelegate.openNewForegroundTab(
+    window,
+    `moz-extension://${extension.uuid}/test.html`
+  );
+
+  await extension.awaitMessage("framed-done");
+
+  extension.sendMessage("close-web-tab");
+  await extension.awaitMessage("web-tab-closed");
+
+  await AppTestDelegate.removeTab(window, tab);
+  await extension.unload();
+});
+
 add_task(async function test_webauthn_forbidden_rpid() {
   let extension = loadTestExtension(forbidden_rpid_script);
 
@@ -363,6 +385,65 @@ async function extension_origin_rpid_script() {
   browser.test.sendMessage("done");
 }
 
+async function framed_rpid_script() {
+  if (window.parent !== window) {
+    // This instance of test.html is framed by a web page, so it is hosted by
+    // the web page's process and cannot claim www.example.com as an RP ID.
+    //
+    // Note: Bug 2044384 may move extension frames to the extension process,
+    // which would make IsWebAuthnAllowedInContext accept this request and
+    // cause the test to fail.
+    let challenge = new Uint8Array(32);
+    crypto.getRandomValues(challenge);
+
+    await browser.test.assertRejects(
+      navigator.credentials.get({
+        publicKey: {
+          rpId: "www.example.com",
+          allowCredentials: [
+            {
+              type: "public-key",
+              id: crypto.getRandomValues(new Uint8Array(32)),
+            },
+          ],
+          challenge,
+        },
+      }),
+      /The operation is insecure/,
+      "get() from an extension frame in a web page throws SecurityError"
+    );
+
+    browser.test.sendMessage("framed-done");
+    return;
+  }
+
+  let tabReady = new Promise(resolve => {
+    browser.tabs.onUpdated.addListener((id, { status }, { url }) => {
+      if (status === "complete" && url === "https://www.example.com/") {
+        resolve();
+      }
+    });
+  });
+  const tab = await browser.tabs.create({ url: "https://www.example.com/" });
+  await tabReady;
+
+  browser.test.onMessage.addListener(async () => {
+    await browser.tabs.remove(tab.id);
+    browser.test.sendMessage("web-tab-closed");
+  });
+
+  await browser.scripting.executeScript({
+    target: { tabId: tab.id },
+    func: frameUrl => {
+      let frame = document.createElement("iframe");
+      frame.setAttribute("allow", "publickey-credentials-get");
+      frame.setAttribute("src", frameUrl);
+      document.body.appendChild(frame);
+    },
+    args: [browser.runtime.getURL("test.html")],
+  });
+}
+
 async function forbidden_rpid_script() {
   let challenge = new Uint8Array(32);
   crypto.getRandomValues(challenge);
Loading diff…