diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 776f0570249..05495472d30 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -70,6 +70,7 @@ import { UserNotificationSettingsService, UserNotificationSettingsServiceAbstraction, } from "@bitwarden/common/autofill/services/user-notification-settings.service"; +import { isUrlInList } from "@bitwarden/common/autofill/utils"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; @@ -1384,18 +1385,11 @@ export default class MainBackground { const tab = await BrowserApi.getTabFromCurrentWindow(); if (tab) { - const currentUriIsBlocked = await firstValueFrom( + const currentUrlIsBlocked = await firstValueFrom( this.domainSettingsService.blockedInteractionsUris$.pipe( - map((blockedInteractionsUris) => { - if (blockedInteractionsUris && tab?.url?.length) { - const tabURL = new URL(tab.url); - const tabIsBlocked = Object.keys(blockedInteractionsUris).some((blockedHostname) => - tabURL.hostname.endsWith(blockedHostname), - ); - - if (tabIsBlocked) { - return true; - } + map((blockedInteractionsUrls) => { + if (blockedInteractionsUrls && tab?.url?.length) { + return isUrlInList(tab.url, blockedInteractionsUrls); } return false; @@ -1403,7 +1397,7 @@ export default class MainBackground { ), ); - await this.cipherContextMenuHandler?.update(tab.url, currentUriIsBlocked); + await this.cipherContextMenuHandler?.update(tab.url, currentUrlIsBlocked); this.onUpdatedRan = this.onReplacedRan = false; } } diff --git a/apps/browser/src/platform/services/browser-script-injector.service.ts b/apps/browser/src/platform/services/browser-script-injector.service.ts index c2bace669dc..a617f2215c0 100644 --- a/apps/browser/src/platform/services/browser-script-injector.service.ts +++ b/apps/browser/src/platform/services/browser-script-injector.service.ts @@ -1,6 +1,7 @@ import { firstValueFrom } from "rxjs"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { isUrlInList } from "@bitwarden/common/autofill/utils"; // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -39,23 +40,20 @@ export class BrowserScriptInjectorService extends ScriptInjectorService { } const tab = tabId && (await BrowserApi.getTab(tabId)); - const tabURL = tab?.url ? new URL(tab.url) : null; - // Check if the tab URI is on the disabled URIs list + // Check if the tab URL is on the disabled URLs list let injectionAllowedInTab = true; const blockedDomains = await firstValueFrom( this.domainSettingsService.blockedInteractionsUris$, ); - if (blockedDomains && tabURL?.hostname) { - const blockedDomainsSet = new Set(Object.keys(blockedDomains)); - - injectionAllowedInTab = !(tabURL && blockedDomainsSet.has(tabURL.hostname)); + if (blockedDomains && tab?.url) { + injectionAllowedInTab = !isUrlInList(tab?.url, blockedDomains); } if (!injectionAllowedInTab) { this.logService.warning( - `${injectDetails.file} was not injected because ${tabURL?.hostname || "the tab URI"} is on the user's blocked domains list.`, + `${injectDetails.file} was not injected because ${tab?.url || "the tab URL"} is on the user's blocked domains list.`, ); return; } diff --git a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts index c65661fdfb2..41d76078fdb 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts @@ -16,10 +16,12 @@ import { import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { isUrlInList } from "@bitwarden/common/autofill/utils"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -70,14 +72,9 @@ export class VaultPopupAutofillService { this.domainSettingsService.blockedInteractionsUris$, this.currentAutofillTab$, ]).pipe( - map(([blockedInteractionsUris, currentTab]) => { - if (blockedInteractionsUris && currentTab?.url?.length) { - const tabURL = new URL(currentTab.url); - const tabIsBlocked = Object.keys(blockedInteractionsUris).includes(tabURL.hostname); - - if (tabIsBlocked) { - return true; - } + map(([blockedInteractionsUrls, currentTab]) => { + if (blockedInteractionsUrls && currentTab) { + return isUrlInList(currentTab?.url, blockedInteractionsUrls); } return false; @@ -89,13 +86,18 @@ export class VaultPopupAutofillService { this.domainSettingsService.blockedInteractionsUris$, this.currentAutofillTab$, ]).pipe( - map(([blockedInteractionsUris, currentTab]) => { - if (blockedInteractionsUris && currentTab?.url?.length) { - const tabURL = new URL(currentTab.url); - const tabIsBlocked = Object.keys(blockedInteractionsUris).includes(tabURL.hostname); + map(([blockedInteractionsUrls, currentTab]) => { + if (blockedInteractionsUrls && currentTab?.url?.length) { + const tabHostname = Utils.getHostname(currentTab.url); + + if (!tabHostname) { + return false; + } + + const tabIsBlocked = isUrlInList(currentTab.url, blockedInteractionsUrls); const showScriptInjectionIsBlockedBanner = - tabIsBlocked && !blockedInteractionsUris[tabURL.hostname]?.bannerIsDismissed; + tabIsBlocked && !blockedInteractionsUrls[tabHostname]?.bannerIsDismissed; return showScriptInjectionIsBlockedBanner; } @@ -108,20 +110,22 @@ export class VaultPopupAutofillService { async dismissCurrentTabIsBlockedBanner() { try { const currentTab = await firstValueFrom(this.currentAutofillTab$); - const currentTabURL = currentTab?.url.length && new URL(currentTab.url); - - const currentTabHostname = currentTabURL && currentTabURL.hostname; + const currentTabHostname = currentTab?.url.length && Utils.getHostname(currentTab.url); if (!currentTabHostname) { return; } - const blockedURIs = await firstValueFrom(this.domainSettingsService.blockedInteractionsUris$); - const tabIsBlocked = Object.keys(blockedURIs).includes(currentTabHostname); + const blockedURLs = await firstValueFrom(this.domainSettingsService.blockedInteractionsUris$); + + let tabIsBlocked = false; + if (blockedURLs && currentTab?.url?.length) { + tabIsBlocked = isUrlInList(currentTab.url, blockedURLs); + } if (tabIsBlocked) { void this.domainSettingsService.setBlockedInteractionsUris({ - ...blockedURIs, + ...blockedURLs, [currentTabHostname as string]: { bannerIsDismissed: true }, }); } @@ -129,7 +133,7 @@ export class VaultPopupAutofillService { // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { throw new Error( - "There was a problem dismissing the blocked interaction URI notification banner", + "There was a problem dismissing the blocked interaction URL notification banner", ); } } @@ -147,11 +151,9 @@ export class VaultPopupAutofillService { } return this.domainSettingsService.blockedInteractionsUris$.pipe( - switchMap((blockedURIs) => { - // This blocked URI logic will be updated to use the common util in PM-18219 - if (blockedURIs && tab?.url?.length) { - const tabURL = new URL(tab.url); - const tabIsBlocked = Object.keys(blockedURIs).includes(tabURL.hostname); + switchMap((blockedURLs) => { + if (blockedURLs && tab?.url?.length) { + const tabIsBlocked = isUrlInList(tab.url, blockedURLs); if (tabIsBlocked) { return of([]); diff --git a/libs/common/src/autofill/utils.spec.ts b/libs/common/src/autofill/utils.spec.ts index 554dc973b48..93f515aa338 100644 --- a/libs/common/src/autofill/utils.spec.ts +++ b/libs/common/src/autofill/utils.spec.ts @@ -1,6 +1,13 @@ +import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; + import { CardView } from "../vault/models/view/card.view"; -import { normalizeExpiryYearFormat, isCardExpired, parseYearMonthExpiry } from "./utils"; +import { + isCardExpired, + isUrlInList, + normalizeExpiryYearFormat, + parseYearMonthExpiry, +} from "./utils"; function getExpiryYearValueFormats(currentCentury: string) { return [ @@ -281,3 +288,73 @@ describe("parseYearMonthExpiry", () => { }); }); }); + +describe("isUrlInList", () => { + let mockUrlList: NeverDomains; + + it("returns false if the passed URL list is empty", () => { + const urlIsInList = isUrlInList("", mockUrlList); + + expect(urlIsInList).toEqual(false); + }); + + it("returns true if the URL hostname is on the passed URL list", () => { + mockUrlList = { + ["bitwarden.com"]: { bannerIsDismissed: true }, + ["duckduckgo.com"]: null, + [".lan"]: null, + [".net"]: null, + ["localhost"]: null, + ["extensions"]: null, + }; + + const testPages = [ + "https://www.bitwarden.com/landing-page?some_query_string_key=1&another_one=1", + " https://duckduckgo.com/pro ", // Note: embedded whitespacing is intentional + "https://network-private-domain.lan/homelabs-dashboard", + "https://jsfiddle.net/", + "https://localhost:8443/#/login", + "chrome://extensions/", + ]; + + for (const pageUrl of testPages) { + const urlIsInList = isUrlInList(pageUrl, mockUrlList); + + expect(urlIsInList).toEqual(true); + } + }); + + it("returns false if no items on the passed URL list are a full match for the page hostname", () => { + const urlIsInList = isUrlInList("https://paypal.com/", { + ["some.packed.subdomains.sandbox.paypal.com"]: null, + }); + + expect(urlIsInList).toEqual(false); + }); + + it("returns false if the URL hostname is not on the passed URL list", () => { + const testPages = ["https://archive.org/", "bitwarden.com.some.otherdomain.com"]; + + for (const pageUrl of testPages) { + const urlIsInList = isUrlInList(pageUrl, mockUrlList); + + expect(urlIsInList).toEqual(false); + } + }); + + it("returns false if the passed URL is empty", () => { + const urlIsInList = isUrlInList("", mockUrlList); + + expect(urlIsInList).toEqual(false); + }); + + it("returns false if the passed URL is not a valid URL", () => { + const testPages = ["twasbrillingandtheslithytoves", "/landing-page", undefined]; + + for (const pageUrl of testPages) { + const urlIsInList = isUrlInList(pageUrl, mockUrlList); + + expect(urlIsInList).toEqual(false); + } + }); +}); diff --git a/libs/common/src/autofill/utils.ts b/libs/common/src/autofill/utils.ts index a77ea8a715d..3078e15bce2 100644 --- a/libs/common/src/autofill/utils.ts +++ b/libs/common/src/autofill/utils.ts @@ -1,3 +1,6 @@ +import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; + import { CardView } from "../vault/models/view/card.view"; import { @@ -329,3 +332,29 @@ export function parseYearMonthExpiry(combinedExpiryValue: string): [Year | null, return [parsedYear, parsedMonth]; } + +/** + * Takes a URL string and a NeverDomains object and determines if the passed URL's hostname is in `urlList` + * + * @param {string} url - representation of URL to check + * @param {NeverDomains} urlList - object with hostname key names + */ +export function isUrlInList(url: string = "", urlList: NeverDomains = {}): boolean { + const urlListKeys = urlList && Object.keys(urlList); + + if (urlListKeys.length && url?.length) { + let tabHostname; + try { + tabHostname = Utils.getHostname(url); + } catch { + // If the input was invalid, exit early and return false + return false; + } + + if (tabHostname) { + return urlListKeys.some((blockedHostname) => tabHostname.endsWith(blockedHostname)); + } + } + + return false; +}