tor-browser

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

commit 11e03be7e93435c02bcb061003ec663ce70f1c55
parent 21f0d24306349a194bb883c002733841f9d86bd7
Author: groovecoder <71928+groovecoder@users.noreply.github.com>
Date:   Wed,  1 Oct 2025 15:24:56 +0000

Bug 1991452 - feat(relay): support denylist filtering of Firefox Relay autocomplete r=credential-management-reviewers,vprema,dimi

Don't show Firefox Relay autocomplete on sites on a new Remote Settings-based denylist,
complementing the existing allowlist logic. This allows finer-grained control over which
sites are offered Relay addresses, especially when the user is signed in.

- Introduced `fxrelay-denylist` Remote Settings collection.
- Updated `FirefoxRelay.sys.mjs` to check denylist entries via a new `onDenyList` function.
- Refactored list loading logic into a reusable `getListCollection()` helper.
- Adjusted test utilities to support stubbing both allow and deny lists.
- Added a new browser test (`browser_relay_use.js`) to verify denylist behavior when a user is signed in.

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

Diffstat:
Mtoolkit/components/passwordmgr/test/browser/browser.toml | 3+++
Mtoolkit/components/passwordmgr/test/browser/browser_relay_signup_flow_showToAllBrowsers.js | 2+-
Mtoolkit/components/passwordmgr/test/browser/browser_relay_telemetry.js | 13++++---------
Atoolkit/components/passwordmgr/test/browser/browser_relay_use.js | 35+++++++++++++++++++++++++++++++++++
Mtoolkit/components/passwordmgr/test/browser/browser_relay_utils.js | 25+++++++++++++++++++++++++
Mtoolkit/components/satchel/integrations/FirefoxRelay.sys.mjs | 97++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
6 files changed, 140 insertions(+), 35 deletions(-)

diff --git a/toolkit/components/passwordmgr/test/browser/browser.toml b/toolkit/components/passwordmgr/test/browser/browser.toml @@ -257,6 +257,9 @@ support-files = ["browser_relay_utils.js"] ["browser_relay_telemetry.js"] support-files = ["browser_relay_utils.js"] +["browser_relay_use.js"] +support-files = ["browser_relay_utils.js"] + ["browser_telemetry_SignUpFormRuleset.js"] ["browser_test_changeContentInputValue.js"] diff --git a/toolkit/components/passwordmgr/test/browser/browser_relay_signup_flow_showToAllBrowsers.js b/toolkit/components/passwordmgr/test/browser/browser_relay_signup_flow_showToAllBrowsers.js @@ -111,7 +111,7 @@ add_task( Assert.equal( rsSandbox.getFakes()[0].callCount, rsSandboxRemoteSettingsGetCallsBeforeSecondACPopup, - "FirefoxRelay onAllowList should only call RemoteSettings.get() once." + "FirefoxRelay shouldShowRelay should only call RemoteSettings.get() once." ); rsSandbox.restore(); } diff --git a/toolkit/components/passwordmgr/test/browser/browser_relay_telemetry.js b/toolkit/components/passwordmgr/test/browser/browser_relay_telemetry.js @@ -5,13 +5,6 @@ Services.scriptloader.loadSubScript( const TEST_URL_PATH = `https://example.org${DIRECTORY_PATH}form_basic_signup.html`; -const setupRelayScenario = async scenarioName => { - await SpecialPowers.pushPrefEnv({ - set: [["signon.firefoxRelay.feature", scenarioName]], - }); - Services.telemetry.clearEvents(); -}; - const collectRelayTelemeryEvent = sameFlow => { const collectedEvents = TelemetryTestUtils.getEvents( { category: "relay_integration" }, @@ -263,7 +256,8 @@ add_task(async function test_popup_option_optin_disabled() { add_task(async function test_popup_option_fillusername() { await setupRelayScenario("enabled"); - const rsSandbox = await stubRemoteSettingsAllowList(); + const rsAllowSandbox = await stubRemoteSettingsAllowList(); + const rsDenySandbox = await stubRemoteSettingsDenyList(); await BrowserTestUtils.withNewTab( { gBrowser, @@ -284,7 +278,8 @@ add_task(async function test_popup_option_fillusername() { ]); } ); - rsSandbox.restore(); + rsAllowSandbox.restore(); + rsDenySandbox.restore(); }); add_task(async function test_fillusername_free_tier_limit() { diff --git a/toolkit/components/passwordmgr/test/browser/browser_relay_use.js b/toolkit/components/passwordmgr/test/browser/browser_relay_use.js @@ -0,0 +1,35 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/passwordmgr/test/browser/browser_relay_utils.js", + this +); + +const TEST_URL_PATH = `https://example.org${DIRECTORY_PATH}form_basic_signup.html`; + +add_task( + async function test_site_on_denyList_does_not_show_Relay_to_signed_in_browser() { + await setupRelayScenario("enabled"); + const sandbox = stubFxAccountsToSimulateSignedIn(); + // Set up denylist for "example.org" + const rsSandbox = await stubRemoteSettingsDenyList([ + { domain: "example.org" }, + ]); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_URL_PATH, + }, + async function (browser) { + const popup = document.getElementById("PopupAutoComplete"); + await openACPopup(popup, browser, "#form-basic-username"); + + const relayItem = getRelayItemFromACPopup(popup); + Assert.ok( + !relayItem, + "Relay item SHOULD NOT be present in the autocomplete popup when the site is on the deny-list, even if the user is signed into the browser." + ); + } + ); + sandbox.restore(); + rsSandbox.restore(); + } +); diff --git a/toolkit/components/passwordmgr/test/browser/browser_relay_utils.js b/toolkit/components/passwordmgr/test/browser/browser_relay_utils.js @@ -107,12 +107,30 @@ async function stubRemoteSettingsAllowList( allowList = [{ domain: "example.org" }] ) { const allowListRS = await lazy.RemoteSettings("fxrelay-allowlist"); + // If already stubbed, restore + if (allowListRS.get && allowListRS.get.restore) { + allowListRS.get.restore(); + } const rsSandbox = sinon.createSandbox(); rsSandbox.stub(allowListRS, "get").returns(allowList); allowListRS.emit("sync"); return rsSandbox; } +async function stubRemoteSettingsDenyList( + denyList = [{ domain: "on-denylist.org" }] +) { + const denyListRS = await lazy.RemoteSettings("fxrelay-denylist"); + // If already stubbed, restore + if (denyListRS.get && denyListRS.get.restore) { + denyListRS.get.restore(); + } + const rsSandbox = sinon.createSandbox(); + rsSandbox.stub(denyListRS, "get").returns(denyList); + denyListRS.emit("sync"); + return rsSandbox; +} + add_setup(async function () { const allMessageIds = []; for (const key in autocompleteUXTreatments) { @@ -179,3 +197,10 @@ async function clickButtonAndWaitForPopupToClose(buttonToClick) { buttonToClick.click(); await notificationHiddenEvent; } + +const setupRelayScenario = async scenarioName => { + await SpecialPowers.pushPrefEnv({ + set: [["signon.firefoxRelay.feature", scenarioName]], + }); + Services.telemetry.clearEvents(); +}; diff --git a/toolkit/components/satchel/integrations/FirefoxRelay.sys.mjs b/toolkit/components/satchel/integrations/FirefoxRelay.sys.mjs @@ -38,8 +38,11 @@ const gConfig = (function () { "signon.firefoxRelay.privacy_policy_url" ), allowListForFirstOfferPref: "signon.firefoxRelay.allowListForFirstOffer", + denyListForFutureOffersPref: "signon.firefoxRelay.denyListForFutureOffers", allowListRemoteSettingsCollectionPref: "signon.firefoxRelay.allowListRemoteSettingsCollection", + denyListRemoteSettingsCollectionPref: + "signon.firefoxRelay.denyListRemoteSettingsCollection", }; })(); @@ -108,6 +111,7 @@ const AUTH_TOKEN_ERROR_CODE = 418; let gFlowId; let gAllowListCollection; +let gDenyListCollection; async function getRelayTokenAsync() { try { @@ -276,6 +280,7 @@ function getDisableIntegration(disableStrings, feature) { }, }; } + async function showReusableMasksAsync(browser, origin, error) { const [reusableMasks, status] = await getReusableMasksAsync(browser, origin); if (!reusableMasks) { @@ -466,44 +471,85 @@ function isSignup(scenarioName) { return scenarioName == "SignUpFormScenario"; } -async function onAllowList(origin) { - const allowListForFirstOffer = Services.prefs.getBoolPref( - gConfig.allowListForFirstOfferPref, - true - ); - if (!allowListForFirstOffer) { - return true; - } - if (!origin) { - return false; - } - if (!gAllowListCollection) { - const allowListRemoteSettingsCollection = Services.prefs.getStringPref( - gConfig.allowListRemoteSettingsCollectionPref, - "fxrelay-allowlist" +// Helper to load/cache RemoteSettings collections +async function getListCollection({ + cache, + setCache, + collectionPref, + defaultCollection, +}) { + if (!cache()) { + const collectionName = Services.prefs.getStringPref( + gConfig[collectionPref], + defaultCollection ); try { - gAllowListCollection = await lazy - .RemoteSettings(allowListRemoteSettingsCollection) - .get(); - lazy.RemoteSettings(allowListRemoteSettingsCollection).on("sync", () => { - gAllowListCollection = null; + const list = await lazy.RemoteSettings(collectionName).get(); + setCache(list); + lazy.RemoteSettings(collectionName).on("sync", () => { + setCache(null); }); } catch (ex) { if (ex instanceof lazy.RemoteSettingsClient.UnknownCollectionError) { lazy.log.warn( "Could not get Remote Settings collection.", - gConfig.allowListRemoteSettingsCollection, + collectionPref, ex ); } throw ex; } } + return cache(); +} + +function isOriginInList(list, origin) { const originHost = new URL(origin).host; - return gAllowListCollection.some( - allowListRecord => allowListRecord.domain == originHost + return list.some(record => record.domain == originHost); +} + +async function shouldNotShowRelay(origin) { + const denyListForFutureOffers = Services.prefs.getBoolPref( + gConfig.denyListForFutureOffersPref, + true ); + if (!denyListForFutureOffers) { + return false; + } + if (!origin) { + return true; + } + const list = await getListCollection({ + cache: () => gDenyListCollection, + setCache: v => { + gDenyListCollection = v; + }, + collectionPref: "denyListRemoteSettingsCollectionPref", + defaultCollection: "fxrelay-denylist", + }); + return isOriginInList(list, origin); +} + +async function shouldShowRelay(origin) { + const allowListForFirstOffer = Services.prefs.getBoolPref( + gConfig.allowListForFirstOfferPref, + true + ); + if (!allowListForFirstOffer) { + return true; + } + if (!origin) { + return false; + } + const list = await getListCollection({ + cache: () => gAllowListCollection, + setCache: v => { + gAllowListCollection = v; + }, + collectionPref: "allowListRemoteSettingsCollectionPref", + defaultCollection: "fxrelay-allowlist", + }); + return isOriginInList(list, origin); } class RelayOffered { @@ -511,7 +557,7 @@ class RelayOffered { const hasFxA = await hasFirefoxAccountAsync(); const showRelayOnAllowlistSiteToAllUsers = Services.prefs.getBoolPref(gConfig.showToAllBrowsersPref, false) && - (await onAllowList(origin)); + (await shouldShowRelay(origin)); if ( !hasInput && isSignup(scenarioName) && @@ -869,7 +915,8 @@ class RelayOffered { class RelayEnabled { async *autocompleteItemsAsync(origin, scenarioName, hasInput) { - if (!hasInput && isSignup(scenarioName)) { + const originOnDenyList = await shouldNotShowRelay(origin); + if (!hasInput && isSignup(scenarioName) && !originOnDenyList) { const hasFxA = await hasFirefoxAccountAsync(); const [title] = await formatMessages("firefox-relay-use-mask-title-1");