mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 06:13:38 +00:00
[PM-18219] Normalize blocked domain checks to a common util (#13416)
* normalize blocked domain checks to common util * do not use currentTabIsOnBlocklist$ in showCurrentTabIsBlockedBanner$ resolution * update additional vault popup autofill service cases to use isUrlInList * cleanup and use Utils get hostname instead of tldts directly
This commit is contained in:
@@ -70,6 +70,7 @@ import {
|
|||||||
UserNotificationSettingsService,
|
UserNotificationSettingsService,
|
||||||
UserNotificationSettingsServiceAbstraction,
|
UserNotificationSettingsServiceAbstraction,
|
||||||
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
} 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 { 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 { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
|
||||||
import { ClientType } from "@bitwarden/common/enums";
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
@@ -1384,18 +1385,11 @@ export default class MainBackground {
|
|||||||
const tab = await BrowserApi.getTabFromCurrentWindow();
|
const tab = await BrowserApi.getTabFromCurrentWindow();
|
||||||
|
|
||||||
if (tab) {
|
if (tab) {
|
||||||
const currentUriIsBlocked = await firstValueFrom(
|
const currentUrlIsBlocked = await firstValueFrom(
|
||||||
this.domainSettingsService.blockedInteractionsUris$.pipe(
|
this.domainSettingsService.blockedInteractionsUris$.pipe(
|
||||||
map((blockedInteractionsUris) => {
|
map((blockedInteractionsUrls) => {
|
||||||
if (blockedInteractionsUris && tab?.url?.length) {
|
if (blockedInteractionsUrls && tab?.url?.length) {
|
||||||
const tabURL = new URL(tab.url);
|
return isUrlInList(tab.url, blockedInteractionsUrls);
|
||||||
const tabIsBlocked = Object.keys(blockedInteractionsUris).some((blockedHostname) =>
|
|
||||||
tabURL.hostname.endsWith(blockedHostname),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tabIsBlocked) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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;
|
this.onUpdatedRan = this.onReplacedRan = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
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
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
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 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;
|
let injectionAllowedInTab = true;
|
||||||
const blockedDomains = await firstValueFrom(
|
const blockedDomains = await firstValueFrom(
|
||||||
this.domainSettingsService.blockedInteractionsUris$,
|
this.domainSettingsService.blockedInteractionsUris$,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (blockedDomains && tabURL?.hostname) {
|
if (blockedDomains && tab?.url) {
|
||||||
const blockedDomainsSet = new Set(Object.keys(blockedDomains));
|
injectionAllowedInTab = !isUrlInList(tab?.url, blockedDomains);
|
||||||
|
|
||||||
injectionAllowedInTab = !(tabURL && blockedDomainsSet.has(tabURL.hostname));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!injectionAllowedInTab) {
|
if (!injectionAllowedInTab) {
|
||||||
this.logService.warning(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,12 @@ import {
|
|||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@@ -70,14 +72,9 @@ export class VaultPopupAutofillService {
|
|||||||
this.domainSettingsService.blockedInteractionsUris$,
|
this.domainSettingsService.blockedInteractionsUris$,
|
||||||
this.currentAutofillTab$,
|
this.currentAutofillTab$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(([blockedInteractionsUris, currentTab]) => {
|
map(([blockedInteractionsUrls, currentTab]) => {
|
||||||
if (blockedInteractionsUris && currentTab?.url?.length) {
|
if (blockedInteractionsUrls && currentTab) {
|
||||||
const tabURL = new URL(currentTab.url);
|
return isUrlInList(currentTab?.url, blockedInteractionsUrls);
|
||||||
const tabIsBlocked = Object.keys(blockedInteractionsUris).includes(tabURL.hostname);
|
|
||||||
|
|
||||||
if (tabIsBlocked) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -89,13 +86,18 @@ export class VaultPopupAutofillService {
|
|||||||
this.domainSettingsService.blockedInteractionsUris$,
|
this.domainSettingsService.blockedInteractionsUris$,
|
||||||
this.currentAutofillTab$,
|
this.currentAutofillTab$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(([blockedInteractionsUris, currentTab]) => {
|
map(([blockedInteractionsUrls, currentTab]) => {
|
||||||
if (blockedInteractionsUris && currentTab?.url?.length) {
|
if (blockedInteractionsUrls && currentTab?.url?.length) {
|
||||||
const tabURL = new URL(currentTab.url);
|
const tabHostname = Utils.getHostname(currentTab.url);
|
||||||
const tabIsBlocked = Object.keys(blockedInteractionsUris).includes(tabURL.hostname);
|
|
||||||
|
if (!tabHostname) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabIsBlocked = isUrlInList(currentTab.url, blockedInteractionsUrls);
|
||||||
|
|
||||||
const showScriptInjectionIsBlockedBanner =
|
const showScriptInjectionIsBlockedBanner =
|
||||||
tabIsBlocked && !blockedInteractionsUris[tabURL.hostname]?.bannerIsDismissed;
|
tabIsBlocked && !blockedInteractionsUrls[tabHostname]?.bannerIsDismissed;
|
||||||
|
|
||||||
return showScriptInjectionIsBlockedBanner;
|
return showScriptInjectionIsBlockedBanner;
|
||||||
}
|
}
|
||||||
@@ -108,20 +110,22 @@ export class VaultPopupAutofillService {
|
|||||||
async dismissCurrentTabIsBlockedBanner() {
|
async dismissCurrentTabIsBlockedBanner() {
|
||||||
try {
|
try {
|
||||||
const currentTab = await firstValueFrom(this.currentAutofillTab$);
|
const currentTab = await firstValueFrom(this.currentAutofillTab$);
|
||||||
const currentTabURL = currentTab?.url.length && new URL(currentTab.url);
|
const currentTabHostname = currentTab?.url.length && Utils.getHostname(currentTab.url);
|
||||||
|
|
||||||
const currentTabHostname = currentTabURL && currentTabURL.hostname;
|
|
||||||
|
|
||||||
if (!currentTabHostname) {
|
if (!currentTabHostname) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const blockedURIs = await firstValueFrom(this.domainSettingsService.blockedInteractionsUris$);
|
const blockedURLs = await firstValueFrom(this.domainSettingsService.blockedInteractionsUris$);
|
||||||
const tabIsBlocked = Object.keys(blockedURIs).includes(currentTabHostname);
|
|
||||||
|
let tabIsBlocked = false;
|
||||||
|
if (blockedURLs && currentTab?.url?.length) {
|
||||||
|
tabIsBlocked = isUrlInList(currentTab.url, blockedURLs);
|
||||||
|
}
|
||||||
|
|
||||||
if (tabIsBlocked) {
|
if (tabIsBlocked) {
|
||||||
void this.domainSettingsService.setBlockedInteractionsUris({
|
void this.domainSettingsService.setBlockedInteractionsUris({
|
||||||
...blockedURIs,
|
...blockedURLs,
|
||||||
[currentTabHostname as string]: { bannerIsDismissed: true },
|
[currentTabHostname as string]: { bannerIsDismissed: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -129,7 +133,7 @@ export class VaultPopupAutofillService {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
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(
|
return this.domainSettingsService.blockedInteractionsUris$.pipe(
|
||||||
switchMap((blockedURIs) => {
|
switchMap((blockedURLs) => {
|
||||||
// This blocked URI logic will be updated to use the common util in PM-18219
|
if (blockedURLs && tab?.url?.length) {
|
||||||
if (blockedURIs && tab?.url?.length) {
|
const tabIsBlocked = isUrlInList(tab.url, blockedURLs);
|
||||||
const tabURL = new URL(tab.url);
|
|
||||||
const tabIsBlocked = Object.keys(blockedURIs).includes(tabURL.hostname);
|
|
||||||
|
|
||||||
if (tabIsBlocked) {
|
if (tabIsBlocked) {
|
||||||
return of([]);
|
return of([]);
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
|
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||||
|
|
||||||
import { CardView } from "../vault/models/view/card.view";
|
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) {
|
function getExpiryYearValueFormats(currentCentury: string) {
|
||||||
return [
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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 { CardView } from "../vault/models/view/card.view";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -329,3 +332,29 @@ export function parseYearMonthExpiry(combinedExpiryValue: string): [Year | null,
|
|||||||
|
|
||||||
return [parsedYear, parsedMonth];
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user