mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-21 16:18:28 +01:00
[PM-4531] Do not override webauthn on excluded domains (#6790)
* [PM-4531] feat: bypass fido2 if origin present in neverDomains * [PM-4531] feat: bypass fido2 during asserts as well * [PM-4531] fix: crashes when using `localhost` * [PM-4531] fix: add missing check * [PM-4531] fix: broken TLD logic * [PM-4531] feat: only allow localhost
This commit is contained in:
parent
665aa2fc0d
commit
62e1e165c4
@ -598,6 +598,7 @@ export default class MainBackground {
|
||||
this.fido2AuthenticatorService,
|
||||
this.configService,
|
||||
this.authService,
|
||||
this.stateService,
|
||||
this.logService
|
||||
);
|
||||
|
||||
|
@ -23,6 +23,36 @@ describe("validateRpId", () => {
|
||||
expect(isValidRpId(rpId, origin)).toBe(false);
|
||||
});
|
||||
|
||||
it("should not be valid when rpId and origin are both different TLD", () => {
|
||||
const rpId = "bitwarden";
|
||||
const origin = "localhost";
|
||||
|
||||
expect(isValidRpId(rpId, origin)).toBe(false);
|
||||
});
|
||||
|
||||
// Only allow localhost for rpId, need to properly investigate the implications of
|
||||
// adding support for ip-addresses and other TLDs
|
||||
it("should not be valid when rpId and origin are both the same TLD", () => {
|
||||
const rpId = "bitwarden";
|
||||
const origin = "bitwarden";
|
||||
|
||||
expect(isValidRpId(rpId, origin)).toBe(false);
|
||||
});
|
||||
|
||||
it("should not be valid when rpId and origin are ip-addresses", () => {
|
||||
const rpId = "127.0.0.1";
|
||||
const origin = "127.0.0.1";
|
||||
|
||||
expect(isValidRpId(rpId, origin)).toBe(false);
|
||||
});
|
||||
|
||||
it("should be valid when domains of rpId and origin are localhost", () => {
|
||||
const rpId = "localhost";
|
||||
const origin = "https://localhost:8080";
|
||||
|
||||
expect(isValidRpId(rpId, origin)).toBe(true);
|
||||
});
|
||||
|
||||
it("should be valid when domains of rpId and origin are the same", () => {
|
||||
const rpId = "bitwarden.com";
|
||||
const origin = "https://bitwarden.com";
|
||||
|
@ -5,7 +5,11 @@ export function isValidRpId(rpId: string, origin: string) {
|
||||
const parsedRpId = parse(rpId, { allowPrivateDomains: true });
|
||||
|
||||
return (
|
||||
parsedOrigin.domain === parsedRpId.domain &&
|
||||
parsedOrigin.subdomain.endsWith(parsedRpId.subdomain)
|
||||
(parsedOrigin.domain == null &&
|
||||
parsedOrigin.hostname == parsedRpId.hostname &&
|
||||
parsedOrigin.hostname == "localhost") ||
|
||||
(parsedOrigin.domain != null &&
|
||||
parsedOrigin.domain == parsedRpId.domain &&
|
||||
parsedOrigin.subdomain.endsWith(parsedRpId.subdomain))
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction";
|
||||
import { StateService } from "../../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import {
|
||||
Fido2AuthenticatorError,
|
||||
@ -27,6 +28,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
let authenticator!: MockProxy<Fido2AuthenticatorService>;
|
||||
let configService!: MockProxy<ConfigServiceAbstraction>;
|
||||
let authService!: MockProxy<AuthService>;
|
||||
let stateService!: MockProxy<StateService>;
|
||||
let client!: Fido2ClientService;
|
||||
let tab!: chrome.tabs.Tab;
|
||||
|
||||
@ -34,8 +36,9 @@ describe("FidoAuthenticatorService", () => {
|
||||
authenticator = mock<Fido2AuthenticatorService>();
|
||||
configService = mock<ConfigServiceAbstraction>();
|
||||
authService = mock<AuthService>();
|
||||
stateService = mock<StateService>();
|
||||
|
||||
client = new Fido2ClientService(authenticator, configService, authService);
|
||||
client = new Fido2ClientService(authenticator, configService, authService, stateService);
|
||||
configService.getFeatureFlag.mockResolvedValue(true);
|
||||
tab = { id: 123, windowId: 456 } as chrome.tabs.Tab;
|
||||
});
|
||||
@ -97,7 +100,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
it("should throw error if rp.id is not valid for this origin", async () => {
|
||||
const params = createParams({
|
||||
origin: "https://passwordless.dev",
|
||||
rp: { id: "bitwarden.com", name: "Bitwraden" },
|
||||
rp: { id: "bitwarden.com", name: "Bitwarden" },
|
||||
});
|
||||
|
||||
const result = async () => await client.createCredential(params, tab);
|
||||
@ -107,10 +110,22 @@ describe("FidoAuthenticatorService", () => {
|
||||
await rejects.toBeInstanceOf(DOMException);
|
||||
});
|
||||
|
||||
it("should fallback if origin hostname is found in neverDomains", async () => {
|
||||
const params = createParams({
|
||||
origin: "https://bitwarden.com",
|
||||
rp: { id: "bitwarden.com", name: "Bitwarden" },
|
||||
});
|
||||
stateService.getNeverDomains.mockResolvedValue({ "bitwarden.com": null });
|
||||
|
||||
const result = async () => await client.createCredential(params, tab);
|
||||
|
||||
await expect(result).rejects.toThrow(FallbackRequestedError);
|
||||
});
|
||||
|
||||
it("should throw error if origin is not an https domain", async () => {
|
||||
const params = createParams({
|
||||
origin: "http://passwordless.dev",
|
||||
rp: { id: "bitwarden.com", name: "Bitwraden" },
|
||||
rp: { id: "bitwarden.com", name: "Bitwarden" },
|
||||
});
|
||||
|
||||
const result = async () => await client.createCredential(params, tab);
|
||||
@ -295,6 +310,17 @@ describe("FidoAuthenticatorService", () => {
|
||||
await rejects.toBeInstanceOf(DOMException);
|
||||
});
|
||||
|
||||
it("should fallback if origin hostname is found in neverDomains", async () => {
|
||||
const params = createParams({
|
||||
origin: "https://bitwarden.com",
|
||||
});
|
||||
stateService.getNeverDomains.mockResolvedValue({ "bitwarden.com": null });
|
||||
|
||||
const result = async () => await client.assertCredential(params, tab);
|
||||
|
||||
await expect(result).rejects.toThrow(FallbackRequestedError);
|
||||
});
|
||||
|
||||
it("should throw error if origin is not an http domain", async () => {
|
||||
const params = createParams({
|
||||
origin: "http://passwordless.dev",
|
||||
|
@ -5,6 +5,7 @@ import { AuthenticationStatus } from "../../../auth/enums/authentication-status"
|
||||
import { FeatureFlag } from "../../../enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { StateService } from "../../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import {
|
||||
Fido2AuthenticatorError,
|
||||
@ -40,6 +41,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
||||
private authenticator: Fido2AuthenticatorService,
|
||||
private configService: ConfigServiceAbstraction,
|
||||
private authService: AuthService,
|
||||
private stateService: StateService,
|
||||
private logService?: LogService
|
||||
) {}
|
||||
|
||||
@ -84,6 +86,12 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
||||
const parsedOrigin = parse(params.origin, { allowPrivateDomains: true });
|
||||
params.rp.id = params.rp.id ?? parsedOrigin.hostname;
|
||||
|
||||
const neverDomains = await this.stateService.getNeverDomains();
|
||||
if (neverDomains != null && parsedOrigin.hostname in neverDomains) {
|
||||
this.logService?.warning(`[Fido2Client] Excluded domain`);
|
||||
throw new FallbackRequestedError();
|
||||
}
|
||||
|
||||
if (parsedOrigin.hostname == undefined || !params.origin.startsWith("https://")) {
|
||||
this.logService?.warning(`[Fido2Client] Invalid https origin: ${params.origin}`);
|
||||
throw new DOMException("'origin' is not a valid https origin", "SecurityError");
|
||||
@ -211,15 +219,15 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
||||
throw new FallbackRequestedError();
|
||||
}
|
||||
|
||||
const { domain: effectiveDomain } = parse(params.origin, { allowPrivateDomains: true });
|
||||
if (effectiveDomain == undefined) {
|
||||
this.logService?.warning(`[Fido2Client] Invalid origin: ${params.origin}`);
|
||||
throw new DOMException("'origin' is not a valid domain", "SecurityError");
|
||||
}
|
||||
|
||||
const parsedOrigin = parse(params.origin, { allowPrivateDomains: true });
|
||||
params.rpId = params.rpId ?? parsedOrigin.hostname;
|
||||
|
||||
const neverDomains = await this.stateService.getNeverDomains();
|
||||
if (neverDomains != null && parsedOrigin.hostname in neverDomains) {
|
||||
this.logService?.warning(`[Fido2Client] Excluded domain`);
|
||||
throw new FallbackRequestedError();
|
||||
}
|
||||
|
||||
if (parsedOrigin.hostname == undefined || !params.origin.startsWith("https://")) {
|
||||
this.logService?.warning(`[Fido2Client] Invalid https origin: ${params.origin}`);
|
||||
throw new DOMException("'origin' is not a valid https origin", "SecurityError");
|
||||
|
Loading…
Reference in New Issue
Block a user