tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 4b2b3d0b71cb05a8e73cb5b92c22d332f27f275e
parent 0b0db692d859aee994b012a512b713140ba1e825
Author: groovecoder <71928+groovecoder@users.noreply.github.com>
Date:   Wed, 22 Oct 2025 19:58:50 +0000

Bug 1993540 - feat(relay): improve isOriginInList matching logic r=credential-management-reviewers,dimi,mtigley

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.
- Add `accounts.example.com.ar` to privileged server locations for both HTTP and HTTPS

Differential Revision: https://phabricator.services.mozilla.com/D268179

Diffstat:
Mbuild/pgo/certs/cert9.db | 0
Mbuild/pgo/certs/key4.db | 0
Mbuild/pgo/certs/mochitest.client | 0
Mbuild/pgo/server-locations.txt | 4++++
Mtoolkit/components/passwordmgr/test/browser/browser_relay_signup_flow.js | 5+++--
Mtoolkit/components/passwordmgr/test/browser/browser_relay_use.js | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/components/satchel/integrations/FirefoxRelay.sys.mjs | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Atoolkit/components/satchel/test/unit/test_isOriginInList.js | 42++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/components/satchel/test/unit/xpcshell.toml | 2++
9 files changed, 253 insertions(+), 8 deletions(-)

diff --git a/build/pgo/certs/cert9.db b/build/pgo/certs/cert9.db Binary files differ. diff --git a/build/pgo/certs/key4.db b/build/pgo/certs/key4.db Binary files differ. diff --git a/build/pgo/certs/mochitest.client b/build/pgo/certs/mochitest.client Binary files differ. diff --git a/build/pgo/server-locations.txt b/build/pgo/server-locations.txt @@ -413,3 +413,7 @@ https://includesubdomains.preloaded.test:443 privileged # Profiler URL to test profile uploads https://api.profiler.firefox.com:443 + +# FirefoxRelay test country and PSL-specific deny- and allow-list matching +http://accounts.example.com.ar:80 privileged +https://accounts.example.com.ar:443 privileged 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: ["example.com.ar"], + allowlist: ["example.com"], + url: "https://accounts.example.com.ar", + expectRelayByScenario: { + available: false, + offered: false, + enabled: false, + disabled: false, + }, + }, + { + desc: ".com on denylist, .com.ar on allowlist", + denylist: ["example.com"], + allowlist: ["example.com.ar"], + url: "https://accounts.example.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"]