commit 1a3b0fc91e28806351f744d95a8701465c81947e
parent 0a310bf4d7c6921c559a7ffb248c164c6095c7cc
Author: groovecoder <71928+groovecoder@users.noreply.github.com>
Date: Mon, 20 Oct 2025 18:44:20 +0000
Bug 1993540 - feat(relay): improve isOriginInList matching logic r=credential-management-reviewers,dimi
Enhance `isOriginInList` with flexible domain matching using PSL-aware normalization and subdomain logic.
This allows correct handling of subdomains, TLD variants, and edge cases (e.g., `www.`, PSL boundaries, localhost).
- Extend `isOriginInList` with normalization via `Services.uriFixup` and `Services.eTLD`
- Add `test_isOriginInList.js` with a suite of cases reflecting Relay's intended matching.
- Add comprehensive tests for various domain allowlist/denylist scenarios in `browser_relay_use.js`, including subdomains and country TLD edge cases.
- Refactor `RelayOffered` logic to improve handling of allowlist, show-to-all flag, and feature pref lock,
ensuring correct relay offering in complex configurations.
Differential Revision: https://phabricator.services.mozilla.com/D268179
Diffstat:
5 files changed, 249 insertions(+), 8 deletions(-)
diff --git a/toolkit/components/passwordmgr/test/browser/browser_relay_signup_flow.js b/toolkit/components/passwordmgr/test/browser/browser_relay_signup_flow.js
@@ -52,7 +52,8 @@ add_task(async function test_default_displays_Relay_to_signed_in_browser() {
});
add_task(
- async function test_site_not_on_allowList_still_shows_Relay_to_signed_in_browser() {
+ async function test_site_not_on_allowList_still_shows_Relay_to_browser_that_already_enabled() {
+ await setupRelayScenario("enabled");
const sandbox = stubFxAccountsToSimulateSignedIn();
const rsSandbox = await stubRemoteSettingsAllowList([
{ domain: "not-example.org" },
@@ -69,7 +70,7 @@ add_task(
const relayItem = getRelayItemFromACPopup(popup);
Assert.ok(
relayItem,
- "Relay item SHOULD be present in the autocomplete popup when the site is not on the allow-list, if the user is signed into the browser."
+ "Relay item SHOULD be present in the autocomplete popup when the site is not on the allow-list, if the browser previously enabled Relay."
);
}
);
diff --git a/toolkit/components/passwordmgr/test/browser/browser_relay_use.js b/toolkit/components/passwordmgr/test/browser/browser_relay_use.js
@@ -35,3 +35,121 @@ add_task(
rsSandbox.restore();
}
);
+
+add_task(async function test_domain_allow_denylist_across_scenarios() {
+ const scenarios = [
+ {
+ desc: "On denylist, not on allowlist",
+ denylist: ["example.org"],
+ allowlist: [],
+ url: "https://example.org",
+ expectRelayByScenario: {
+ available: false,
+ offered: false,
+ enabled: false,
+ disabled: false,
+ },
+ },
+ {
+ desc: "On allowlist, not on denylist",
+ denylist: [],
+ allowlist: ["example.org"],
+ url: "https://example.org",
+ expectRelayByScenario: {
+ available: true,
+ offered: true,
+ enabled: true,
+ disabled: false,
+ },
+ },
+ {
+ desc: "Not on allowlist or denylist",
+ denylist: [],
+ allowlist: [],
+ url: "https://test1.example.com",
+ expectRelayByScenario: {
+ available: false,
+ offered: false,
+ enabled: true,
+ disabled: false,
+ },
+ },
+ {
+ desc: "Subdomain on denylist, parent on allowlist",
+ denylist: ["test2.example.com"],
+ allowlist: ["example.com"],
+ url: "https://test2.example.com",
+ expectRelayByScenario: {
+ available: false,
+ offered: false,
+ enabled: false,
+ disabled: false,
+ },
+ },
+ {
+ desc: "Country TLD on denylist, .com on allowlist",
+ denylist: ["google.com.ar"],
+ allowlist: ["google.com"],
+ url: "https://accounts.google.com.ar",
+ expectRelayByScenario: {
+ available: false,
+ offered: false,
+ enabled: false,
+ disabled: false,
+ },
+ },
+ {
+ desc: ".com on denylist, .com.ar on allowlist",
+ denylist: ["google.com"],
+ allowlist: ["google.com.ar"],
+ url: "https://accounts.google.com.ar",
+ expectRelayByScenario: {
+ available: true,
+ offered: true,
+ enabled: true,
+ disabled: false,
+ },
+ },
+ ];
+
+ for (const scenario of scenarios) {
+ info(`Test: ${scenario.desc}`);
+ const sandbox = stubFxAccountsToSimulateSignedIn();
+
+ const rsSandboxDeny = await stubRemoteSettingsDenyList(
+ scenario.denylist.map(domain => ({ domain }))
+ );
+ const rsSandboxAllow = await stubRemoteSettingsAllowList(
+ scenario.allowlist.map(domain => ({ domain }))
+ );
+
+ for (const relayScenario of [
+ "available",
+ "offered",
+ "enabled",
+ "disabled",
+ ]) {
+ await setupRelayScenario(relayScenario);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `${scenario.url}${DIRECTORY_PATH}form_basic_signup.html`,
+ },
+ async function (browser) {
+ const popup = document.getElementById("PopupAutoComplete");
+ await openACPopup(popup, browser, "#form-basic-username");
+ const relayItem = getRelayItemFromACPopup(popup);
+ const expected = scenario.expectRelayByScenario[relayScenario];
+ Assert.equal(
+ !!relayItem,
+ expected,
+ `Relay item should${expected ? "" : " NOT"} be present (${scenario.desc}, relayScenario=${relayScenario})`
+ );
+ }
+ );
+ }
+ sandbox.restore();
+ rsSandboxDeny && rsSandboxDeny.restore();
+ rsSandboxAllow && rsSandboxAllow.restore();
+ }
+});
diff --git a/toolkit/components/satchel/integrations/FirefoxRelay.sys.mjs b/toolkit/components/satchel/integrations/FirefoxRelay.sys.mjs
@@ -503,9 +503,79 @@ async function getListCollection({
return cache();
}
+/**
+ * Checks if the origin matches a record in the list according to Relay rules:
+ * using flexible normalization and PSL via Services.uriFixup.
+ +---------------------------+-----------------------------------+--------+
+ | list | origin | Result |
+ +---------------------------+-----------------------------------+--------+
+ | google.com | https://google.com | True |
+ | google.com | https://www.google.com | True |
+ | www.google.com | https://www.google.com | True |
+ | google.com.ar | https://accounts.google.com.ar | True |
+ | google.com.ar | https://google.com | False |
+ | google.com | https://google.com.ar | True |
+ | mozilla.org | https://vpn.mozilla.org | True |
+ | vpn.mozilla.org | https://vpn.mozilla.org | True |
+ | substack.com | https://hunterharris.substack.com | True |
+ | hunterharris.substack.com | https://hunterharris.substack.com | True |
+ | hunterharris.substack.com | https://other.substack.com | False |
+ | example.co.uk | https://foo.example.co.uk | True |
+ | localhost | http://localhost | True |
+ | google.com.ar | https://mail.google.com.br | False |
+ +---------------------------+-----------------------------------+--------+
+ *
+ * @param {Array} list Array of {domain: ...} records. Each domain is a string.
+ * @param {string} origin Origin URL (e.g., https://www.google.com.ar).
+ * @returns {boolean}
+ */
function isOriginInList(list, origin) {
- const originHost = new URL(origin).host;
- return list.some(record => record.domain == originHost);
+ let host;
+ try {
+ // PSL-aware, normalized results via uriFixup
+ const { fixedURI } = Services.uriFixup.getFixupURIInfo(origin);
+ if (!fixedURI) {
+ return false;
+ }
+ host = fixedURI.host;
+ } catch {
+ return false;
+ }
+
+ // 1. Exact host match (e.g. 'www.foo.com' in list)
+ if (list.some(record => record.domain === host)) {
+ return true;
+ }
+
+ // 2. PSL-aware subdomain/root match
+ if (
+ list.some(record => {
+ try {
+ return Services.eTLD.hasRootDomain(host, record.domain);
+ } catch {
+ return false;
+ }
+ })
+ ) {
+ return true;
+ }
+
+ // 3. Special case: "universal" domain match, e.g. allowlist has "google.com" and origin is "google.com.ar"
+ // Only apply for domains ending with common one-level TLDs
+ const UNIVERSAL_TLDS = [".com", ".org", ".net", ".edu", ".gov"];
+ for (const record of list) {
+ for (const tld of UNIVERSAL_TLDS) {
+ if (
+ record.domain.endsWith(tld) &&
+ host.length > record.domain.length &&
+ host.startsWith(record.domain + ".")
+ ) {
+ return true;
+ }
+ }
+ }
+
+ return false;
}
async function shouldNotShowRelay(origin) {
@@ -559,14 +629,21 @@ class RelayOffered {
return;
}
const hasFxA = await hasFirefoxAccountAsync();
+ const showToAllBrowsersPrefEnabled = Services.prefs.getBoolPref(
+ gConfig.showToAllBrowsersPref,
+ false
+ );
+ const relayShouldShow = await shouldShowRelay(origin);
const showRelayOnAllowlistSiteToAllUsers =
- Services.prefs.getBoolPref(gConfig.showToAllBrowsersPref, false) &&
- (await shouldShowRelay(origin));
+ showToAllBrowsersPrefEnabled && relayShouldShow;
+ const relayFeaturePrefUnlocked = !Services.prefs.prefIsLocked(
+ gConfig.relayFeaturePref
+ );
if (
!hasInput &&
isSignup(scenarioName) &&
- !Services.prefs.prefIsLocked(gConfig.relayFeaturePref) &&
- (hasFxA || showRelayOnAllowlistSiteToAllUsers)
+ relayFeaturePrefUnlocked &&
+ (showRelayOnAllowlistSiteToAllUsers || relayShouldShow)
) {
const nimbusRelayAutocompleteFeature =
lazy.NimbusFeatures["email-autocomplete-relay"];
@@ -1004,4 +1081,5 @@ class RelayFeature extends OptInFeature {
}
}
+export { isOriginInList };
export const FirefoxRelay = new RelayFeature();
diff --git a/toolkit/components/satchel/test/unit/test_isOriginInList.js b/toolkit/components/satchel/test/unit/test_isOriginInList.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+const { isOriginInList } = ChromeUtils.importESModule(
+ "resource://gre/modules/FirefoxRelay.sys.mjs"
+);
+
+// Helper to construct allow/deny lists like production:
+function makeList(arr) {
+ return arr.map(domain => ({ domain }));
+}
+
+// Table: [listEntry, origin, expected]
+const TESTS = [
+ ["google.com", "https://google.com", true],
+ ["google.com", "https://www.google.com", true],
+ ["www.google.com", "https://www.google.com", true],
+ ["google.com.ar", "https://accounts.google.com.ar", true],
+ ["google.com.ar", "https://google.com", false],
+ ["google.com", "https://google.com.ar", true],
+ ["mozilla.org", "https://vpn.mozilla.org", true],
+ ["vpn.mozilla.org", "https://vpn.mozilla.org", true],
+ ["substack.com", "https://hunterharris.substack.com", true],
+ ["hunterharris.substack.com", "https://hunterharris.substack.com", true],
+ ["hunterharris.substack.com", "https://other.substack.com", false],
+ ["example.co.uk", "https://foo.example.co.uk", true],
+ ["localhost", "http://localhost", true],
+ ["google.com.ar", "https://mail.google.com.br", false],
+];
+
+add_task(async function test_isOriginInList() {
+ for (let [listEntry, origin, expected] of TESTS) {
+ let list = makeList([listEntry]);
+ let result = isOriginInList(list, origin);
+ Assert.equal(
+ result,
+ expected,
+ `isOriginInList([${listEntry}], ${origin}) === ${expected}`
+ );
+ }
+});
diff --git a/toolkit/components/satchel/test/unit/xpcshell.toml b/toolkit/components/satchel/test/unit/xpcshell.toml
@@ -36,4 +36,6 @@ skip-if = ["condprof"] # Bug 1769154 - not supported
["test_history_sources.js"]
+["test_isOriginInList.js"]
+
["test_notify.js"]