commit dc24b8290977f4bc8186f6f4c21522e421287c02
parent 9b1c48431ea80e6b0098ddfc111b210ee8c00e31
Author: Benjamin VanderSloot <bvandersloot@mozilla.com>
Date: Fri, 3 Oct 2025 14:04:25 +0000
Bug 1989281 - Elevate the permission dialog for wallet custom schemes on Desktop - r=fluent-reviewers,emz,bolsson
Manual testing is easiest if you add `mailto` or `zoomus` to
`WALLET_SCHEMES` to find an easy example of a link with a registered
protocol handler on your system.
Differential Revision: https://phabricator.services.mozilla.com/D265275
Diffstat:
7 files changed, 201 insertions(+), 9 deletions(-)
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
@@ -1242,6 +1242,9 @@ pref("privacy.globalprivacycontrol.functionality.enabled", true);
// Enable GPC in private browsing mode
pref("privacy.globalprivacycontrol.pbmode.enabled", true);
+// What custom schemes to treat as accessing digital wallets, comma separated
+pref("privacy.wallet_schemes", "openid4vp,mdoc,mdoc-openid4vp,haip,eudi-wallet,eudi-openid4vp,openid-credential-offer");
+
pref("network.proxy.share_proxy_settings", false); // use the same proxy settings for all protocols
// simple gestures support
diff --git a/toolkit/components/nimbus/FeatureManifest.yaml b/toolkit/components/nimbus/FeatureManifest.yaml
@@ -5623,3 +5623,16 @@ image:
setPref:
branch: user
pref: "image.mem.decode_bytes_at_a_time"
+
+walletCustomSchemes:
+ description: Defines which custom schemes are treated as digital identity requests
+ owner: bvandersloot@mozilla.com
+ hasExposure: false
+ variables:
+ enabledSchemes:
+ type: string
+ setPref:
+ branch: user
+ pref: privacy.wallet_schemes
+ description: >-
+ Comma separated lists of schemes
diff --git a/toolkit/locales/en-US/toolkit/global/handlerDialog.ftl b/toolkit/locales/en-US/toolkit/global/handlerDialog.ftl
@@ -39,6 +39,22 @@ permission-dialog-description-system-app =
permission-dialog-description-system-noapp =
Open the { $scheme } link?
+# Header on a permission prompt, asking the user if they should allow the website to continue.
+wallet-custom-scheme-warning-heading =
+ .heading = Allow this site to open your digital wallet?
+
+# Warning given to the user that the current page is trying to open their digital wallet app
+wallet-custom-scheme-warning-host-app = Opening a <strong>{ $scheme }</strong> link allows <strong>{ $host }</strong> to request your real identity with <strong>{ $appName }</strong>. Only continue if you trust this site.
+
+# Warning given to the user that the current page is trying to open their digital wallet app
+wallet-custom-scheme-warning-app = Opening a <strong>{ $scheme }</strong> link allows this site to request your real identity with <strong>{ $appName }</strong>. Only continue if you trust this site.
+
+# Warning given to the user that the current page is trying to open their digital wallet app
+wallet-custom-scheme-warning-host = Opening a <strong>{ $scheme }</strong> link allows <strong>{ $host }</strong> to request your real identity from your digital wallet. Only continue if you trust this site.
+
+# Warning given to the user that the current page is trying to open their digital wallet app
+wallet-custom-scheme-warning = Opening a <strong>{ $scheme }</strong> link allows this site to request your real identity from your digital wallet. Only continue if you trust this site.
+
## Please keep the emphasis around the hostname and scheme (ie the
## `<strong>` HTML tags). Please also keep the hostname as close to the start
## of the sentence as your language's grammar allows.
diff --git a/toolkit/mozapps/handling/content/handler.css b/toolkit/mozapps/handling/content/handler.css
@@ -28,6 +28,7 @@ description {
#description,
#description-box,
+#warning-bar,
#rememberContainer,
#chooser {
margin: 0 4px 16px;
diff --git a/toolkit/mozapps/handling/content/permissionDialog.js b/toolkit/mozapps/handling/content/permissionDialog.js
@@ -5,6 +5,20 @@
const { EnableDelayHelper } = ChromeUtils.importESModule(
"resource://gre/modules/PromptUtils.sys.mjs"
);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+// These schemes are for digital identitity documents.
+// We show a warning card for them in the permission dialog,
+// so the user is made aware the site may be requesting a real
+// identity.
+const lazy = {};
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "walletSchemes",
+ "privacy.wallet_schemes"
+);
let dialog = {
/**
@@ -145,6 +159,26 @@ let dialog = {
},
/**
+ * Determines the l10n ID to use for the dangerous scheme warning,
+ * depending on the triggering principal and the preferred application
+ * handler.
+ */
+ get walletWarningL10nId() {
+ if (this.shouldShowPrincipal() && this.userReadablePrincipal) {
+ if (this._preferredHandlerName) {
+ return "wallet-custom-scheme-warning-host-app";
+ }
+ return "wallet-custom-scheme-warning-host";
+ }
+
+ if (this._preferredHandlerName) {
+ return "wallet-custom-scheme-warning-app";
+ }
+
+ return "wallet-custom-scheme-warning";
+ },
+
+ /**
* Computes text to show in the prompt that is a user-understandable
* version of what is asking to open the external protocol.
* It's usually the prePath of the site that wants to navigate to
@@ -214,6 +248,23 @@ let dialog = {
scheme,
});
}
+
+ let walletSchemeList = lazy.walletSchemes.split(",");
+ if (walletSchemeList.includes(scheme)) {
+ let warning = document.getElementById("warning-bar");
+ document.l10n.setAttributes(
+ warning,
+ "wallet-custom-scheme-warning-heading"
+ );
+ warning.messageL10nId = this.walletWarningL10nId;
+ warning.messageL10nArgs = {
+ host,
+ scheme,
+ appName: this._preferredHandlerName,
+ };
+ warning.hidden = false;
+ description.hidden = true;
+ }
},
onAccept() {
diff --git a/toolkit/mozapps/handling/content/permissionDialog.xhtml b/toolkit/mozapps/handling/content/permissionDialog.xhtml
@@ -32,8 +32,15 @@
type="application/javascript"
/>
+ <script
+ type="module"
+ src="chrome://global/content/elements/moz-message-bar.mjs"
+ ></script>
+
<vbox id="description-box">
<description id="description"></description>
+ <html:moz-message-bar type="warning" id="warning-bar" hidden="true">
+ </html:moz-message-bar>
<label
id="change-app"
hidden="true"
diff --git a/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js
@@ -32,6 +32,7 @@ const PROTOCOL_HANDLER_OPEN_PERM_KEY = "open-protocol-handler";
const PERMISSION_KEY_DELIMITER = "^";
const TEST_PROTOS = ["foo", "bar"];
+const WALLET_PROTO = "moz-test-wallet";
let testDir = getChromeDir(getResolvedURI(gTestPath));
@@ -76,7 +77,9 @@ function getSystemProtocol() {
* Creates dummy web protocol handlers used for testing.
*/
function initTestHandlers() {
- TEST_PROTOS.forEach(scheme => {
+ const allProtos = structuredClone(TEST_PROTOS);
+ allProtos.push(WALLET_PROTO);
+ allProtos.forEach(scheme => {
let webHandler = Cc[
"@mozilla.org/uriloader/web-handler-app;1"
].createInstance(Ci.nsIWebHandlerApp);
@@ -223,6 +226,7 @@ async function testOpenProto(
actionConfirm,
actionChangeApp,
checkContents,
+ hasWalletWarning = false,
} = permDialogOptions;
if (actionChangeApp) {
@@ -230,15 +234,35 @@ async function testOpenProto(
}
let descriptionEl = dialogEl.querySelector("#description");
- ok(
- descriptionEl && BrowserTestUtils.isVisible(descriptionEl),
- "Has a visible description element."
- );
+ let warningEl = dialogEl.querySelector("#warning-bar");
+ if (hasWalletWarning) {
+ ok(
+ descriptionEl && !BrowserTestUtils.isVisible(descriptionEl),
+ "Has an invisible description element."
+ );
+ ok(
+ warningEl && BrowserTestUtils.isVisible(warningEl),
+ "Has a visible warning element."
+ );
+ ok(
+ !warningEl.innerHTML.toLowerCase().includes(NULL_PRINCIPAL_SCHEME),
+ "Warning does not include NullPrincipal scheme."
+ );
+ } else {
+ ok(
+ descriptionEl && BrowserTestUtils.isVisible(descriptionEl),
+ "Has a visible description element."
+ );
- ok(
- !descriptionEl.innerHTML.toLowerCase().includes(NULL_PRINCIPAL_SCHEME),
- "Description does not include NullPrincipal scheme."
- );
+ ok(
+ !descriptionEl.innerHTML.toLowerCase().includes(NULL_PRINCIPAL_SCHEME),
+ "Description does not include NullPrincipal scheme."
+ );
+ ok(
+ warningEl && !BrowserTestUtils.isVisible(warningEl),
+ "Has an invisible warning element."
+ );
+ }
await testCheckbox(dialogEl, dialogType, {
hasCheckbox,
@@ -466,6 +490,9 @@ registerCleanupFunction(function () {
add_setup(async function () {
initTestHandlers();
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.wallet_schemes", WALLET_PROTO]],
+ });
});
/**
@@ -1378,3 +1405,77 @@ add_task(async function test_unloaded_iframe() {
});
});
});
+
+/**
+ * Tests that we show a warning UI element for a wallet scheme
+ */
+add_task(async function test_prompt_warning_for_wallet_scheme() {
+ // Test that we show the warning in the simple case.
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, WALLET_PROTO, {
+ permDialogOptions: {
+ hasCheckbox: true,
+ hasChangeApp: false,
+ chooserIsNext: true,
+ actionConfirm: true,
+ hasWalletWarning: true,
+ },
+ chooserDialogOptions: { hasCheckbox: true, actionConfirm: true },
+ });
+ });
+
+ // Test that we show the warning with a null principal
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, WALLET_PROTO, {
+ triggerLoad: () => {
+ let uri = `${WALLET_PROTO}://test`;
+ ContentTask.spawn(browser, { uri }, args => {
+ let frame = content.document.createElement("iframe");
+ frame.src = `data:text/html,<script>location.href="${args.uri}"</script>`;
+ content.document.body.appendChild(frame);
+ });
+ },
+ permDialogOptions: {
+ hasCheckbox: false,
+ chooserIsNext: true,
+ hasChangeApp: false,
+ actionConfirm: true,
+ hasWalletWarning: true,
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+
+ // Test that we show the warning with a system principal
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, WALLET_PROTO, {
+ permDialogOptions: {
+ hasCheckbox: false,
+ hasChangeApp: false,
+ chooserIsNext: true,
+ actionChangeApp: false,
+ hasWalletWarning: true,
+ },
+ triggerLoad: useTriggeringPrincipal(
+ Services.scriptSecurityManager.getSystemPrincipal()
+ ),
+ });
+ });
+
+ // Test that we don't show the warning for another scheme.
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, TEST_PROTOS[0], {
+ permDialogOptions: {
+ hasCheckbox: true,
+ hasChangeApp: false,
+ chooserIsNext: true,
+ actionConfirm: true,
+ hasWalletWarning: false,
+ },
+ chooserDialogOptions: { hasCheckbox: true, actionConfirm: true },
+ });
+ });
+});