mirror of
https://github.com/bitwarden/browser.git
synced 2024-09-30 04:28:19 +02:00
[PM-1426] Refactor uri matching (#5003)
* Move URI matching logic into uriView * Fix url parsing: always assign default protocol, otherwise no protocol with port is parsed incorrectly * Codescene: refactor domain matching logic
This commit is contained in:
parent
576d85b268
commit
7899b25ab3
@ -1,4 +1,3 @@
|
|||||||
import { UriMatchType } from "@bitwarden/common/enums";
|
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
import AutofillField from "../../models/autofill-field";
|
import AutofillField from "../../models/autofill-field";
|
||||||
@ -40,9 +39,4 @@ export abstract class AutofillService {
|
|||||||
fromCommand: boolean
|
fromCommand: boolean
|
||||||
) => Promise<string>;
|
) => Promise<string>;
|
||||||
doAutoFillActiveTab: (pageDetails: PageDetail[], fromCommand: boolean) => Promise<string>;
|
doAutoFillActiveTab: (pageDetails: PageDetail[], fromCommand: boolean) => Promise<string>;
|
||||||
iframeUrlMatches: (
|
|
||||||
pageUrl: string,
|
|
||||||
loginItem: CipherView,
|
|
||||||
defaultUriMatch: UriMatchType
|
|
||||||
) => boolean;
|
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,11 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
|
|||||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
||||||
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
||||||
import { EventType, FieldType, UriMatchType } from "@bitwarden/common/enums";
|
import { EventType, FieldType, UriMatchType } from "@bitwarden/common/enums";
|
||||||
import { Utils } from "@bitwarden/common/misc/utils";
|
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
||||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
|
||||||
|
|
||||||
import { BrowserApi } from "../../browser/browserApi";
|
import { BrowserApi } from "../../browser/browserApi";
|
||||||
import { BrowserStateService } from "../../services/abstractions/browser-state.service";
|
import { BrowserStateService } from "../../services/abstractions/browser-state.service";
|
||||||
@ -349,9 +347,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
fillScript.savedUrls =
|
fillScript.savedUrls =
|
||||||
login?.uris?.filter((u) => u.match != UriMatchType.Never).map((u) => u.uri) ?? [];
|
login?.uris?.filter((u) => u.match != UriMatchType.Never).map((u) => u.uri) ?? [];
|
||||||
|
|
||||||
const inIframe = pageDetails.url !== options.tabUrl;
|
fillScript.untrustedIframe = this.inUntrustedIframe(pageDetails.url, options);
|
||||||
fillScript.untrustedIframe =
|
|
||||||
inIframe && !this.iframeUrlMatches(pageDetails.url, options.cipher, options.defaultUriMatch);
|
|
||||||
|
|
||||||
if (!login.password || login.password === "") {
|
if (!login.password || login.password === "") {
|
||||||
// No password for this login. Maybe they just wanted to auto-fill some custom fields?
|
// No password for this login. Maybe they just wanted to auto-fill some custom fields?
|
||||||
@ -787,81 +783,28 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether to warn the user about filling an iframe
|
* Determines whether an iframe is potentially dangerous ("untrusted") to autofill
|
||||||
* @param pageUrl The url of the page/iframe, usually from AutofillPageDetails
|
* @param pageUrl The url of the page/iframe, usually from AutofillPageDetails
|
||||||
* @param tabUrl The url of the tab, usually from the message sender (should not come from a content script because
|
* @param options The GenerateFillScript options
|
||||||
* that is likely to be incorrect in the case of iframes)
|
* @returns `true` if the iframe is untrusted and a warning should be shown, `false` otherwise
|
||||||
* @param loginItem The cipher to be filled
|
|
||||||
* @returns `true` if the iframe is untrusted and the warning should be shown, `false` otherwise
|
|
||||||
*/
|
*/
|
||||||
iframeUrlMatches(pageUrl: string, loginItem: CipherView, defaultUriMatch: UriMatchType): boolean {
|
private inUntrustedIframe(pageUrl: string, options: GenerateFillScriptOptions): boolean {
|
||||||
|
// If the pageUrl (from the content script) matches the tabUrl (from the sender tab), we are not in an iframe
|
||||||
|
// This also avoids a false positive if no URI is saved and the user triggers auto-fill anyway
|
||||||
|
if (pageUrl === options.tabUrl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Check the pageUrl against cipher URIs using the configured match detection.
|
// Check the pageUrl against cipher URIs using the configured match detection.
|
||||||
// If we are in this function at all, it is assumed that the tabUrl already matches a URL for `loginItem`,
|
// Remember: if we are in this function, the tabUrl already matches a saved URI for the login.
|
||||||
// need to verify the pageUrl also matches one of the saved URIs using the match detection selected.
|
// We need to verify the pageUrl also matches.
|
||||||
const uriMatched = loginItem.login.uris?.some((uri) =>
|
const equivalentDomains = this.settingsService.getEquivalentDomains(pageUrl);
|
||||||
this.uriMatches(uri, pageUrl, defaultUriMatch)
|
const matchesUri = options.cipher.login.matchesUri(
|
||||||
|
pageUrl,
|
||||||
|
equivalentDomains,
|
||||||
|
options.defaultUriMatch
|
||||||
);
|
);
|
||||||
|
return !matchesUri;
|
||||||
return uriMatched;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO should this be put in a common place (Utils maybe?) to be used both here and by CipherService?
|
|
||||||
private uriMatches(uri: LoginUriView, url: string, defaultUriMatch: UriMatchType): boolean {
|
|
||||||
const matchType = uri.match ?? defaultUriMatch;
|
|
||||||
|
|
||||||
const matchDomains = [Utils.getDomain(url)];
|
|
||||||
const equivalentDomains = this.settingsService.getEquivalentDomains(url);
|
|
||||||
if (equivalentDomains != null) {
|
|
||||||
matchDomains.push(...equivalentDomains);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (matchType) {
|
|
||||||
case UriMatchType.Domain:
|
|
||||||
if (url != null && uri.domain != null && matchDomains.includes(uri.domain)) {
|
|
||||||
if (Utils.DomainMatchBlacklist.has(uri.domain)) {
|
|
||||||
const domainUrlHost = Utils.getHost(url);
|
|
||||||
if (!Utils.DomainMatchBlacklist.get(uri.domain).has(domainUrlHost)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UriMatchType.Host: {
|
|
||||||
const urlHost = Utils.getHost(url);
|
|
||||||
if (urlHost != null && urlHost === Utils.getHost(uri.uri)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case UriMatchType.Exact:
|
|
||||||
if (url === uri.uri) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UriMatchType.StartsWith:
|
|
||||||
if (url.startsWith(uri.uri)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UriMatchType.RegularExpression:
|
|
||||||
try {
|
|
||||||
const regex = new RegExp(uri.uri, "i");
|
|
||||||
if (regex.test(url)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UriMatchType.Never:
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fieldAttrsContain(field: AutofillField, containsVal: string) {
|
private fieldAttrsContain(field: AutofillField, containsVal: string) {
|
||||||
|
@ -309,7 +309,6 @@ export default class MainBackground {
|
|||||||
this.apiService,
|
this.apiService,
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
() => this.searchService,
|
() => this.searchService,
|
||||||
this.logService,
|
|
||||||
this.stateService,
|
this.stateService,
|
||||||
this.encryptService,
|
this.encryptService,
|
||||||
this.cipherFileUploadService
|
this.cipherFileUploadService
|
||||||
|
@ -27,10 +27,6 @@ import {
|
|||||||
i18nServiceFactory,
|
i18nServiceFactory,
|
||||||
I18nServiceInitOptions,
|
I18nServiceInitOptions,
|
||||||
} from "../../../background/service_factories/i18n-service.factory";
|
} from "../../../background/service_factories/i18n-service.factory";
|
||||||
import {
|
|
||||||
logServiceFactory,
|
|
||||||
LogServiceInitOptions,
|
|
||||||
} from "../../../background/service_factories/log-service.factory";
|
|
||||||
import {
|
import {
|
||||||
SettingsServiceInitOptions,
|
SettingsServiceInitOptions,
|
||||||
settingsServiceFactory,
|
settingsServiceFactory,
|
||||||
@ -52,7 +48,6 @@ export type CipherServiceInitOptions = CipherServiceFactoryOptions &
|
|||||||
ApiServiceInitOptions &
|
ApiServiceInitOptions &
|
||||||
CipherFileUploadServiceInitOptions &
|
CipherFileUploadServiceInitOptions &
|
||||||
I18nServiceInitOptions &
|
I18nServiceInitOptions &
|
||||||
LogServiceInitOptions &
|
|
||||||
StateServiceInitOptions &
|
StateServiceInitOptions &
|
||||||
EncryptServiceInitOptions;
|
EncryptServiceInitOptions;
|
||||||
|
|
||||||
@ -73,7 +68,6 @@ export function cipherServiceFactory(
|
|||||||
opts.cipherServiceOptions?.searchServiceFactory === undefined
|
opts.cipherServiceOptions?.searchServiceFactory === undefined
|
||||||
? () => cache.searchService as SearchService
|
? () => cache.searchService as SearchService
|
||||||
: opts.cipherServiceOptions.searchServiceFactory,
|
: opts.cipherServiceOptions.searchServiceFactory,
|
||||||
await logServiceFactory(cache, opts),
|
|
||||||
await stateServiceFactory(cache, opts),
|
await stateServiceFactory(cache, opts),
|
||||||
await encryptServiceFactory(cache, opts),
|
await encryptServiceFactory(cache, opts),
|
||||||
await cipherFileUploadServiceFactory(cache, opts)
|
await cipherFileUploadServiceFactory(cache, opts)
|
||||||
|
@ -245,7 +245,6 @@ export class Main {
|
|||||||
this.apiService,
|
this.apiService,
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
null,
|
null,
|
||||||
this.logService,
|
|
||||||
this.stateService,
|
this.stateService,
|
||||||
this.encryptService,
|
this.encryptService,
|
||||||
this.cipherFileUploadService
|
this.cipherFileUploadService
|
||||||
|
@ -252,7 +252,6 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
|||||||
apiService: ApiServiceAbstraction,
|
apiService: ApiServiceAbstraction,
|
||||||
i18nService: I18nServiceAbstraction,
|
i18nService: I18nServiceAbstraction,
|
||||||
injector: Injector,
|
injector: Injector,
|
||||||
logService: LogService,
|
|
||||||
stateService: StateServiceAbstraction,
|
stateService: StateServiceAbstraction,
|
||||||
encryptService: EncryptService,
|
encryptService: EncryptService,
|
||||||
fileUploadService: CipherFileUploadServiceAbstraction
|
fileUploadService: CipherFileUploadServiceAbstraction
|
||||||
@ -263,7 +262,6 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
|||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
() => injector.get(SearchServiceAbstraction),
|
() => injector.get(SearchServiceAbstraction),
|
||||||
logService,
|
|
||||||
stateService,
|
stateService,
|
||||||
encryptService,
|
encryptService,
|
||||||
fileUploadService
|
fileUploadService
|
||||||
@ -274,7 +272,6 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
|||||||
ApiServiceAbstraction,
|
ApiServiceAbstraction,
|
||||||
I18nServiceAbstraction,
|
I18nServiceAbstraction,
|
||||||
Injector, // TODO: Get rid of this circular dependency!
|
Injector, // TODO: Get rid of this circular dependency!
|
||||||
LogService,
|
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
EncryptService,
|
EncryptService,
|
||||||
CipherFileUploadServiceAbstraction,
|
CipherFileUploadServiceAbstraction,
|
||||||
|
@ -346,4 +346,14 @@ describe("Utils Service", () => {
|
|||||||
).toBe("api/sends/access/sendkey");
|
).toBe("api/sends/access/sendkey");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getUrl", () => {
|
||||||
|
it("assumes a http protocol if no protocol is specified", () => {
|
||||||
|
const urlString = "www.exampleapp.com.au:4000";
|
||||||
|
|
||||||
|
const actual = Utils.getUrl(urlString);
|
||||||
|
|
||||||
|
expect(actual.protocol).toBe("http:");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -17,6 +17,12 @@ describe("SettingsService", () => {
|
|||||||
let activeAccount: BehaviorSubject<string>;
|
let activeAccount: BehaviorSubject<string>;
|
||||||
let activeAccountUnlocked: BehaviorSubject<boolean>;
|
let activeAccountUnlocked: BehaviorSubject<boolean>;
|
||||||
|
|
||||||
|
const mockEquivalentDomains = [
|
||||||
|
["example.com", "exampleapp.com", "example.co.uk", "ejemplo.es"],
|
||||||
|
["bitwarden.com", "bitwarden.co.uk", "sm-bitwarden.com"],
|
||||||
|
["example.co.uk", "exampleapp.co.uk"],
|
||||||
|
];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cryptoService = Substitute.for();
|
cryptoService = Substitute.for();
|
||||||
encryptService = Substitute.for();
|
encryptService = Substitute.for();
|
||||||
@ -24,7 +30,7 @@ describe("SettingsService", () => {
|
|||||||
activeAccount = new BehaviorSubject("123");
|
activeAccount = new BehaviorSubject("123");
|
||||||
activeAccountUnlocked = new BehaviorSubject(true);
|
activeAccountUnlocked = new BehaviorSubject(true);
|
||||||
|
|
||||||
stateService.getSettings().resolves({ equivalentDomains: [["test"], ["domains"]] });
|
stateService.getSettings().resolves({ equivalentDomains: mockEquivalentDomains });
|
||||||
stateService.activeAccount$.returns(activeAccount);
|
stateService.activeAccount$.returns(activeAccount);
|
||||||
stateService.activeAccountUnlocked$.returns(activeAccountUnlocked);
|
stateService.activeAccountUnlocked$.returns(activeAccountUnlocked);
|
||||||
(window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService);
|
(window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService);
|
||||||
@ -38,12 +44,21 @@ describe("SettingsService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("getEquivalentDomains", () => {
|
describe("getEquivalentDomains", () => {
|
||||||
it("returns value", async () => {
|
it("returns all equivalent domains for a URL", async () => {
|
||||||
const result = await firstValueFrom(settingsService.settings$);
|
const actual = settingsService.getEquivalentDomains("example.co.uk");
|
||||||
|
const expected = new Set([
|
||||||
|
"example.com",
|
||||||
|
"exampleapp.com",
|
||||||
|
"example.co.uk",
|
||||||
|
"ejemplo.es",
|
||||||
|
"exampleapp.co.uk",
|
||||||
|
]);
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
expect(result).toEqual({
|
it("returns an empty set if there are no equivalent domains", () => {
|
||||||
equivalentDomains: [["test"], ["domains"]],
|
const actual = settingsService.getEquivalentDomains("asdf");
|
||||||
});
|
expect(actual).toEqual(new Set());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -6,6 +6,6 @@ export abstract class SettingsService {
|
|||||||
settings$: Observable<AccountSettingsSettings>;
|
settings$: Observable<AccountSettingsSettings>;
|
||||||
|
|
||||||
setEquivalentDomains: (equivalentDomains: string[][]) => Promise<any>;
|
setEquivalentDomains: (equivalentDomains: string[][]) => Promise<any>;
|
||||||
getEquivalentDomains: (url: string) => string[];
|
getEquivalentDomains: (url: string) => Set<string>;
|
||||||
clear: (userId?: string) => Promise<void>;
|
clear: (userId?: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -379,15 +379,7 @@ export class Utils {
|
|||||||
|
|
||||||
uriString = uriString.trim();
|
uriString = uriString.trim();
|
||||||
|
|
||||||
let url = Utils.getUrlObject(uriString);
|
return Utils.getUrlObject(uriString);
|
||||||
if (url == null) {
|
|
||||||
const hasHttpProtocol =
|
|
||||||
uriString.indexOf("http://") === 0 || uriString.indexOf("https://") === 0;
|
|
||||||
if (!hasHttpProtocol && uriString.indexOf(".") > -1) {
|
|
||||||
url = Utils.getUrlObject("http://" + uriString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static camelToPascalCase(s: string) {
|
static camelToPascalCase(s: string) {
|
||||||
@ -542,22 +534,21 @@ export class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static getUrlObject(uriString: string): URL {
|
private static getUrlObject(uriString: string): URL {
|
||||||
|
// All the methods below require a protocol to properly parse a URL string
|
||||||
|
// Assume http if no other protocol is present
|
||||||
|
const hasProtocol = uriString.indexOf("://") > -1;
|
||||||
|
if (!hasProtocol && uriString.indexOf(".") > -1) {
|
||||||
|
uriString = "http://" + uriString;
|
||||||
|
} else if (!hasProtocol) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (nodeURL != null) {
|
if (nodeURL != null) {
|
||||||
return new nodeURL.URL(uriString);
|
return new nodeURL.URL(uriString);
|
||||||
} else if (typeof URL === "function") {
|
|
||||||
return new URL(uriString);
|
|
||||||
} else if (typeof window !== "undefined") {
|
|
||||||
const hasProtocol = uriString.indexOf("://") > -1;
|
|
||||||
if (!hasProtocol && uriString.indexOf(".") > -1) {
|
|
||||||
uriString = "http://" + uriString;
|
|
||||||
} else if (!hasProtocol) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const anchor = window.document.createElement("a");
|
|
||||||
anchor.href = uriString;
|
|
||||||
return anchor as any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new URL(uriString);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore error
|
// Ignore error
|
||||||
}
|
}
|
||||||
|
@ -40,10 +40,10 @@ export class SettingsService implements SettingsServiceAbstraction {
|
|||||||
await this.stateService.setSettings(settings);
|
await this.stateService.setSettings(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
getEquivalentDomains(url: string): string[] {
|
getEquivalentDomains(url: string): Set<string> {
|
||||||
const domain = Utils.getDomain(url);
|
const domain = Utils.getDomain(url);
|
||||||
if (domain == null) {
|
if (domain == null) {
|
||||||
return null;
|
return new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
const settings = this._settings.getValue();
|
const settings = this._settings.getValue();
|
||||||
@ -58,7 +58,7 @@ export class SettingsService implements SettingsServiceAbstraction {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return new Set(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clear(userId?: string): Promise<void> {
|
async clear(userId?: string): Promise<void> {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { UriMatchType } from "../../../enums";
|
import { UriMatchType } from "../../../enums";
|
||||||
|
import { Utils } from "../../../misc/utils";
|
||||||
|
|
||||||
import { LoginUriView } from "./login-uri.view";
|
import { LoginUriView } from "./login-uri.view";
|
||||||
|
|
||||||
@ -25,6 +26,18 @@ const testData = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const exampleUris = {
|
||||||
|
standard: "https://www.exampleapp.com.au:4000/userauth/login.html",
|
||||||
|
standardRegex: "https://www.exampleapp.com.au:[0-9]*/[A-Za-z]+/login.html",
|
||||||
|
standardNotMatching: "https://www.exampleapp.com.au:4000/userauth123/login.html",
|
||||||
|
subdomain: "https://www.auth.exampleapp.com.au",
|
||||||
|
differentDomain: "https://www.exampleapp.co.uk/subpage",
|
||||||
|
differentHost: "https://www.exampleapp.com.au/userauth/login.html",
|
||||||
|
equivalentDomains: () =>
|
||||||
|
new Set(["exampleapp.com.au", "exampleapp.com", "exampleapp.co.uk", "example.com"]),
|
||||||
|
noEquivalentDomains: () => new Set<string>(),
|
||||||
|
};
|
||||||
|
|
||||||
describe("LoginUriView", () => {
|
describe("LoginUriView", () => {
|
||||||
it("isWebsite() given an invalid domain should return false", async () => {
|
it("isWebsite() given an invalid domain should return false", async () => {
|
||||||
const uri = new LoginUriView();
|
const uri = new LoginUriView();
|
||||||
@ -63,4 +76,119 @@ describe("LoginUriView", () => {
|
|||||||
Object.assign(uri, { match: UriMatchType.Host, uri: "someprotocol://bitwarden.com" });
|
Object.assign(uri, { match: UriMatchType.Host, uri: "someprotocol://bitwarden.com" });
|
||||||
expect(uri.canLaunch).toBe(false);
|
expect(uri.canLaunch).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("uri matching", () => {
|
||||||
|
describe("using domain matching", () => {
|
||||||
|
it("matches the same domain", () => {
|
||||||
|
const uri = uriFactory(UriMatchType.Domain, exampleUris.standard);
|
||||||
|
const actual = uri.matchesUri(exampleUris.subdomain, exampleUris.noEquivalentDomains());
|
||||||
|
expect(actual).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches equivalent domains", () => {
|
||||||
|
const uri = uriFactory(UriMatchType.Domain, exampleUris.standard);
|
||||||
|
const actual = uri.matchesUri(exampleUris.differentDomain, exampleUris.equivalentDomains());
|
||||||
|
expect(actual).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not match a different domain", () => {
|
||||||
|
const uri = uriFactory(UriMatchType.Domain, exampleUris.standard);
|
||||||
|
const actual = uri.matchesUri(
|
||||||
|
exampleUris.differentDomain,
|
||||||
|
exampleUris.noEquivalentDomains()
|
||||||
|
);
|
||||||
|
expect(actual).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actual integration test with the real blacklist, not ideal
|
||||||
|
it("does not match domains that are blacklisted", () => {
|
||||||
|
const googleEquivalentDomains = new Set(["google.com", "script.google.com"]);
|
||||||
|
const uri = uriFactory(UriMatchType.Domain, "google.com");
|
||||||
|
|
||||||
|
const actual = uri.matchesUri("script.google.com", googleEquivalentDomains);
|
||||||
|
|
||||||
|
expect(actual).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("using host matching", () => {
|
||||||
|
it("matches the same host", () => {
|
||||||
|
const uri = uriFactory(UriMatchType.Host, Utils.getHost(exampleUris.standard));
|
||||||
|
const actual = uri.matchesUri(exampleUris.standard, exampleUris.noEquivalentDomains());
|
||||||
|
expect(actual).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not match a different host", () => {
|
||||||
|
const uri = uriFactory(UriMatchType.Host, Utils.getHost(exampleUris.differentDomain));
|
||||||
|
const actual = uri.matchesUri(exampleUris.standard, exampleUris.noEquivalentDomains());
|
||||||
|
expect(actual).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("using exact matching", () => {
|
||||||
|
it("matches if both uris are the same", () => {
|
||||||
|
const uri = uriFactory(UriMatchType.Exact, exampleUris.standard);
|
||||||
|
const actual = uri.matchesUri(exampleUris.standard, exampleUris.noEquivalentDomains());
|
||||||
|
expect(actual).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not match if the uris are different", () => {
|
||||||
|
const uri = uriFactory(UriMatchType.Exact, exampleUris.standard);
|
||||||
|
const actual = uri.matchesUri(
|
||||||
|
exampleUris.standard + "#",
|
||||||
|
exampleUris.noEquivalentDomains()
|
||||||
|
);
|
||||||
|
expect(actual).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("using startsWith matching", () => {
|
||||||
|
it("matches if the target URI starts with the saved URI", () => {
|
||||||
|
const uri = uriFactory(UriMatchType.StartsWith, exampleUris.standard);
|
||||||
|
const actual = uri.matchesUri(
|
||||||
|
exampleUris.standard + "#bookmark",
|
||||||
|
exampleUris.noEquivalentDomains()
|
||||||
|
);
|
||||||
|
expect(actual).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not match if the start of the uri is not the same", () => {
|
||||||
|
const uri = uriFactory(UriMatchType.StartsWith, exampleUris.standard);
|
||||||
|
const actual = uri.matchesUri(
|
||||||
|
exampleUris.standard.slice(1),
|
||||||
|
exampleUris.noEquivalentDomains()
|
||||||
|
);
|
||||||
|
expect(actual).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("using regular expression matching", () => {
|
||||||
|
it("matches if the regular expression matches", () => {
|
||||||
|
const uri = uriFactory(UriMatchType.RegularExpression, exampleUris.standard);
|
||||||
|
const actual = uri.matchesUri(exampleUris.standardRegex, exampleUris.noEquivalentDomains());
|
||||||
|
expect(actual).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not match if the regular expression does not match", () => {
|
||||||
|
const uri = uriFactory(UriMatchType.RegularExpression, exampleUris.standardNotMatching);
|
||||||
|
const actual = uri.matchesUri(exampleUris.standardRegex, exampleUris.noEquivalentDomains());
|
||||||
|
expect(actual).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("using never matching", () => {
|
||||||
|
it("does not match even if uris are identical", () => {
|
||||||
|
const uri = uriFactory(UriMatchType.Never, exampleUris.standard);
|
||||||
|
const actual = uri.matchesUri(exampleUris.standard, exampleUris.noEquivalentDomains());
|
||||||
|
expect(actual).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function uriFactory(match: UriMatchType, uri: string) {
|
||||||
|
const loginUri = new LoginUriView();
|
||||||
|
loginUri.match = match;
|
||||||
|
loginUri.uri = uri;
|
||||||
|
return loginUri;
|
||||||
|
}
|
||||||
|
@ -129,4 +129,60 @@ export class LoginUriView implements View {
|
|||||||
static fromJSON(obj: Partial<Jsonify<LoginUriView>>): LoginUriView {
|
static fromJSON(obj: Partial<Jsonify<LoginUriView>>): LoginUriView {
|
||||||
return Object.assign(new LoginUriView(), obj);
|
return Object.assign(new LoginUriView(), obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
matchesUri(
|
||||||
|
targetUri: string,
|
||||||
|
equivalentDomains: Set<string>,
|
||||||
|
defaultUriMatch: UriMatchType = null
|
||||||
|
): boolean {
|
||||||
|
if (!this.uri || !targetUri) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let matchType = this.match ?? defaultUriMatch;
|
||||||
|
matchType ??= UriMatchType.Domain;
|
||||||
|
|
||||||
|
const targetDomain = Utils.getDomain(targetUri);
|
||||||
|
const matchDomains = equivalentDomains.add(targetDomain);
|
||||||
|
|
||||||
|
switch (matchType) {
|
||||||
|
case UriMatchType.Domain:
|
||||||
|
return this.matchesDomain(targetUri, matchDomains);
|
||||||
|
case UriMatchType.Host: {
|
||||||
|
const urlHost = Utils.getHost(targetUri);
|
||||||
|
return urlHost != null && urlHost === Utils.getHost(this.uri);
|
||||||
|
}
|
||||||
|
case UriMatchType.Exact:
|
||||||
|
return targetUri === this.uri;
|
||||||
|
case UriMatchType.StartsWith:
|
||||||
|
return targetUri.startsWith(this.uri);
|
||||||
|
case UriMatchType.RegularExpression:
|
||||||
|
try {
|
||||||
|
const regex = new RegExp(this.uri, "i");
|
||||||
|
return regex.test(targetUri);
|
||||||
|
} catch (e) {
|
||||||
|
// Invalid regex
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
case UriMatchType.Never:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private matchesDomain(targetUri: string, matchDomains: Set<string>) {
|
||||||
|
if (targetUri == null || this.domain == null || !matchDomains.has(this.domain)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Utils.DomainMatchBlacklist.has(this.domain)) {
|
||||||
|
const domainUrlHost = Utils.getHost(targetUri);
|
||||||
|
return !Utils.DomainMatchBlacklist.get(this.domain).has(domainUrlHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { LoginLinkedId as LinkedId } from "../../../enums";
|
import { LoginLinkedId as LinkedId, UriMatchType } from "../../../enums";
|
||||||
import { linkedFieldOption } from "../../../misc/linkedFieldOption.decorator";
|
import { linkedFieldOption } from "../../../misc/linkedFieldOption.decorator";
|
||||||
import { Utils } from "../../../misc/utils";
|
import { Utils } from "../../../misc/utils";
|
||||||
import { Login } from "../domain/login";
|
import { Login } from "../domain/login";
|
||||||
@ -63,6 +63,18 @@ export class LoginView extends ItemView {
|
|||||||
return this.uris != null && this.uris.length > 0;
|
return this.uris != null && this.uris.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
matchesUri(
|
||||||
|
targetUri: string,
|
||||||
|
equivalentDomains: Set<string>,
|
||||||
|
defaultUriMatch: UriMatchType = null
|
||||||
|
): boolean {
|
||||||
|
if (this.uris == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.uris.some((uri) => uri.matchesUri(targetUri, equivalentDomains, defaultUriMatch));
|
||||||
|
}
|
||||||
|
|
||||||
static fromJSON(obj: Partial<Jsonify<LoginView>>): LoginView {
|
static fromJSON(obj: Partial<Jsonify<LoginView>>): LoginView {
|
||||||
const passwordRevisionDate =
|
const passwordRevisionDate =
|
||||||
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
|
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
|
||||||
|
@ -5,7 +5,6 @@ import { ApiService } from "../../abstractions/api.service";
|
|||||||
import { CryptoService } from "../../abstractions/crypto.service";
|
import { CryptoService } from "../../abstractions/crypto.service";
|
||||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||||
import { I18nService } from "../../abstractions/i18n.service";
|
import { I18nService } from "../../abstractions/i18n.service";
|
||||||
import { LogService } from "../../abstractions/log.service";
|
|
||||||
import { SearchService } from "../../abstractions/search.service";
|
import { SearchService } from "../../abstractions/search.service";
|
||||||
import { SettingsService } from "../../abstractions/settings.service";
|
import { SettingsService } from "../../abstractions/settings.service";
|
||||||
import { StateService } from "../../abstractions/state.service";
|
import { StateService } from "../../abstractions/state.service";
|
||||||
@ -28,7 +27,6 @@ describe("Cipher Service", () => {
|
|||||||
let cipherFileUploadService: SubstituteOf<CipherFileUploadService>;
|
let cipherFileUploadService: SubstituteOf<CipherFileUploadService>;
|
||||||
let i18nService: SubstituteOf<I18nService>;
|
let i18nService: SubstituteOf<I18nService>;
|
||||||
let searchService: SubstituteOf<SearchService>;
|
let searchService: SubstituteOf<SearchService>;
|
||||||
let logService: SubstituteOf<LogService>;
|
|
||||||
let encryptService: SubstituteOf<EncryptService>;
|
let encryptService: SubstituteOf<EncryptService>;
|
||||||
|
|
||||||
let cipherService: CipherService;
|
let cipherService: CipherService;
|
||||||
@ -41,7 +39,6 @@ describe("Cipher Service", () => {
|
|||||||
cipherFileUploadService = Substitute.for<CipherFileUploadService>();
|
cipherFileUploadService = Substitute.for<CipherFileUploadService>();
|
||||||
i18nService = Substitute.for<I18nService>();
|
i18nService = Substitute.for<I18nService>();
|
||||||
searchService = Substitute.for<SearchService>();
|
searchService = Substitute.for<SearchService>();
|
||||||
logService = Substitute.for<LogService>();
|
|
||||||
encryptService = Substitute.for<EncryptService>();
|
encryptService = Substitute.for<EncryptService>();
|
||||||
|
|
||||||
cryptoService.encryptToBytes(Arg.any(), Arg.any()).resolves(ENCRYPTED_BYTES);
|
cryptoService.encryptToBytes(Arg.any(), Arg.any()).resolves(ENCRYPTED_BYTES);
|
||||||
@ -53,7 +50,6 @@ describe("Cipher Service", () => {
|
|||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
() => searchService,
|
() => searchService,
|
||||||
logService,
|
|
||||||
stateService,
|
stateService,
|
||||||
encryptService,
|
encryptService,
|
||||||
cipherFileUploadService
|
cipherFileUploadService
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
import { firstValueFrom } from "rxjs";
|
|
||||||
|
|
||||||
import { ApiService } from "../../abstractions/api.service";
|
import { ApiService } from "../../abstractions/api.service";
|
||||||
import { CryptoService } from "../../abstractions/crypto.service";
|
import { CryptoService } from "../../abstractions/crypto.service";
|
||||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||||
import { I18nService } from "../../abstractions/i18n.service";
|
import { I18nService } from "../../abstractions/i18n.service";
|
||||||
import { LogService } from "../../abstractions/log.service";
|
|
||||||
import { SearchService } from "../../abstractions/search.service";
|
import { SearchService } from "../../abstractions/search.service";
|
||||||
import { SettingsService } from "../../abstractions/settings.service";
|
import { SettingsService } from "../../abstractions/settings.service";
|
||||||
import { StateService } from "../../abstractions/state.service";
|
import { StateService } from "../../abstractions/state.service";
|
||||||
import { FieldType, UriMatchType } from "../../enums";
|
import { FieldType, UriMatchType } from "../../enums";
|
||||||
import { sequentialize } from "../../misc/sequentialize";
|
import { sequentialize } from "../../misc/sequentialize";
|
||||||
import { Utils } from "../../misc/utils";
|
import { Utils } from "../../misc/utils";
|
||||||
import { AccountSettingsSettings } from "../../models/domain/account";
|
|
||||||
import Domain from "../../models/domain/domain-base";
|
import Domain from "../../models/domain/domain-base";
|
||||||
import { EncArrayBuffer } from "../../models/domain/enc-array-buffer";
|
import { EncArrayBuffer } from "../../models/domain/enc-array-buffer";
|
||||||
import { EncString } from "../../models/domain/enc-string";
|
import { EncString } from "../../models/domain/enc-string";
|
||||||
@ -58,7 +54,6 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private searchService: () => SearchService,
|
private searchService: () => SearchService,
|
||||||
private logService: LogService,
|
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
private cipherFileUploadService: CipherFileUploadService
|
private cipherFileUploadService: CipherFileUploadService
|
||||||
@ -399,37 +394,9 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const domain = Utils.getDomain(url);
|
const equivalentDomains = this.settingsService.getEquivalentDomains(url);
|
||||||
const eqDomainsPromise =
|
const ciphers = await this.getAllDecrypted();
|
||||||
domain == null
|
defaultMatch ??= await this.stateService.getDefaultUriMatch();
|
||||||
? Promise.resolve([])
|
|
||||||
: firstValueFrom(this.settingsService.settings$).then(
|
|
||||||
(settings: AccountSettingsSettings) => {
|
|
||||||
let matches: any[] = [];
|
|
||||||
settings?.equivalentDomains?.forEach((eqDomain: any) => {
|
|
||||||
if (eqDomain.length && eqDomain.indexOf(domain) >= 0) {
|
|
||||||
matches = matches.concat(eqDomain);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!matches.length) {
|
|
||||||
matches.push(domain);
|
|
||||||
}
|
|
||||||
|
|
||||||
return matches;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await Promise.all([eqDomainsPromise, this.getAllDecrypted()]);
|
|
||||||
const matchingDomains = result[0];
|
|
||||||
const ciphers = result[1];
|
|
||||||
|
|
||||||
if (defaultMatch == null) {
|
|
||||||
defaultMatch = await this.stateService.getDefaultUriMatch();
|
|
||||||
if (defaultMatch == null) {
|
|
||||||
defaultMatch = UriMatchType.Domain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ciphers.filter((cipher) => {
|
return ciphers.filter((cipher) => {
|
||||||
if (cipher.deletedDate != null) {
|
if (cipher.deletedDate != null) {
|
||||||
@ -439,59 +406,8 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url != null && cipher.type === CipherType.Login && cipher.login.uris != null) {
|
if (cipher.type === CipherType.Login && cipher.login !== null) {
|
||||||
for (let i = 0; i < cipher.login.uris.length; i++) {
|
return cipher.login.matchesUri(url, equivalentDomains, defaultMatch);
|
||||||
const u = cipher.login.uris[i];
|
|
||||||
if (u.uri == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = u.match == null ? defaultMatch : u.match;
|
|
||||||
switch (match) {
|
|
||||||
case UriMatchType.Domain:
|
|
||||||
if (domain != null && u.domain != null && matchingDomains.indexOf(u.domain) > -1) {
|
|
||||||
if (Utils.DomainMatchBlacklist.has(u.domain)) {
|
|
||||||
const domainUrlHost = Utils.getHost(url);
|
|
||||||
if (!Utils.DomainMatchBlacklist.get(u.domain).has(domainUrlHost)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UriMatchType.Host: {
|
|
||||||
const urlHost = Utils.getHost(url);
|
|
||||||
if (urlHost != null && urlHost === Utils.getHost(u.uri)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case UriMatchType.Exact:
|
|
||||||
if (url === u.uri) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UriMatchType.StartsWith:
|
|
||||||
if (url.startsWith(u.uri)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UriMatchType.RegularExpression:
|
|
||||||
try {
|
|
||||||
const regex = new RegExp(u.uri, "i");
|
|
||||||
if (regex.test(url)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UriMatchType.Never:
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
Loading…
Reference in New Issue
Block a user