Firefox · DOM
CVE-2026-109750
Logic Error in DOM
Overview
Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
add_tasktoolkit/components/extensions/test/mochitest/test_ext_webauthn.html |
modified | |
iftoolkit/components/extensions/test/mochitest/test_ext_webauthn.html |
modified |
Files Changed
dom/webauthn/WebAuthnUtil.cpptoolkit/components/extensions/test/mochitest/test_ext_webauthn.html
Patch
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…
References
On This Page