mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 05:43:41 +00: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:
@@ -598,6 +598,7 @@ export default class MainBackground {
|
|||||||
this.fido2AuthenticatorService,
|
this.fido2AuthenticatorService,
|
||||||
this.configService,
|
this.configService,
|
||||||
this.authService,
|
this.authService,
|
||||||
|
this.stateService,
|
||||||
this.logService
|
this.logService
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,36 @@ describe("validateRpId", () => {
|
|||||||
expect(isValidRpId(rpId, origin)).toBe(false);
|
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", () => {
|
it("should be valid when domains of rpId and origin are the same", () => {
|
||||||
const rpId = "bitwarden.com";
|
const rpId = "bitwarden.com";
|
||||||
const origin = "https://bitwarden.com";
|
const origin = "https://bitwarden.com";
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ export function isValidRpId(rpId: string, origin: string) {
|
|||||||
const parsedRpId = parse(rpId, { allowPrivateDomains: true });
|
const parsedRpId = parse(rpId, { allowPrivateDomains: true });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
parsedOrigin.domain === parsedRpId.domain &&
|
(parsedOrigin.domain == null &&
|
||||||
parsedOrigin.subdomain.endsWith(parsedRpId.subdomain)
|
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 { AuthService } from "../../../auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||||
import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction";
|
import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction";
|
||||||
|
import { StateService } from "../../../platform/abstractions/state.service";
|
||||||
import { Utils } from "../../../platform/misc/utils";
|
import { Utils } from "../../../platform/misc/utils";
|
||||||
import {
|
import {
|
||||||
Fido2AuthenticatorError,
|
Fido2AuthenticatorError,
|
||||||
@@ -27,6 +28,7 @@ describe("FidoAuthenticatorService", () => {
|
|||||||
let authenticator!: MockProxy<Fido2AuthenticatorService>;
|
let authenticator!: MockProxy<Fido2AuthenticatorService>;
|
||||||
let configService!: MockProxy<ConfigServiceAbstraction>;
|
let configService!: MockProxy<ConfigServiceAbstraction>;
|
||||||
let authService!: MockProxy<AuthService>;
|
let authService!: MockProxy<AuthService>;
|
||||||
|
let stateService!: MockProxy<StateService>;
|
||||||
let client!: Fido2ClientService;
|
let client!: Fido2ClientService;
|
||||||
let tab!: chrome.tabs.Tab;
|
let tab!: chrome.tabs.Tab;
|
||||||
|
|
||||||
@@ -34,8 +36,9 @@ describe("FidoAuthenticatorService", () => {
|
|||||||
authenticator = mock<Fido2AuthenticatorService>();
|
authenticator = mock<Fido2AuthenticatorService>();
|
||||||
configService = mock<ConfigServiceAbstraction>();
|
configService = mock<ConfigServiceAbstraction>();
|
||||||
authService = mock<AuthService>();
|
authService = mock<AuthService>();
|
||||||
|
stateService = mock<StateService>();
|
||||||
|
|
||||||
client = new Fido2ClientService(authenticator, configService, authService);
|
client = new Fido2ClientService(authenticator, configService, authService, stateService);
|
||||||
configService.getFeatureFlag.mockResolvedValue(true);
|
configService.getFeatureFlag.mockResolvedValue(true);
|
||||||
tab = { id: 123, windowId: 456 } as chrome.tabs.Tab;
|
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 () => {
|
it("should throw error if rp.id is not valid for this origin", async () => {
|
||||||
const params = createParams({
|
const params = createParams({
|
||||||
origin: "https://passwordless.dev",
|
origin: "https://passwordless.dev",
|
||||||
rp: { id: "bitwarden.com", name: "Bitwraden" },
|
rp: { id: "bitwarden.com", name: "Bitwarden" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = async () => await client.createCredential(params, tab);
|
const result = async () => await client.createCredential(params, tab);
|
||||||
@@ -107,10 +110,22 @@ describe("FidoAuthenticatorService", () => {
|
|||||||
await rejects.toBeInstanceOf(DOMException);
|
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 () => {
|
it("should throw error if origin is not an https domain", async () => {
|
||||||
const params = createParams({
|
const params = createParams({
|
||||||
origin: "http://passwordless.dev",
|
origin: "http://passwordless.dev",
|
||||||
rp: { id: "bitwarden.com", name: "Bitwraden" },
|
rp: { id: "bitwarden.com", name: "Bitwarden" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = async () => await client.createCredential(params, tab);
|
const result = async () => await client.createCredential(params, tab);
|
||||||
@@ -295,6 +310,17 @@ describe("FidoAuthenticatorService", () => {
|
|||||||
await rejects.toBeInstanceOf(DOMException);
|
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 () => {
|
it("should throw error if origin is not an http domain", async () => {
|
||||||
const params = createParams({
|
const params = createParams({
|
||||||
origin: "http://passwordless.dev",
|
origin: "http://passwordless.dev",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { AuthenticationStatus } from "../../../auth/enums/authentication-status"
|
|||||||
import { FeatureFlag } from "../../../enums/feature-flag.enum";
|
import { FeatureFlag } from "../../../enums/feature-flag.enum";
|
||||||
import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction";
|
import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction";
|
||||||
import { LogService } from "../../../platform/abstractions/log.service";
|
import { LogService } from "../../../platform/abstractions/log.service";
|
||||||
|
import { StateService } from "../../../platform/abstractions/state.service";
|
||||||
import { Utils } from "../../../platform/misc/utils";
|
import { Utils } from "../../../platform/misc/utils";
|
||||||
import {
|
import {
|
||||||
Fido2AuthenticatorError,
|
Fido2AuthenticatorError,
|
||||||
@@ -40,6 +41,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
|||||||
private authenticator: Fido2AuthenticatorService,
|
private authenticator: Fido2AuthenticatorService,
|
||||||
private configService: ConfigServiceAbstraction,
|
private configService: ConfigServiceAbstraction,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
|
private stateService: StateService,
|
||||||
private logService?: LogService
|
private logService?: LogService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -84,6 +86,12 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
|||||||
const parsedOrigin = parse(params.origin, { allowPrivateDomains: true });
|
const parsedOrigin = parse(params.origin, { allowPrivateDomains: true });
|
||||||
params.rp.id = params.rp.id ?? parsedOrigin.hostname;
|
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://")) {
|
if (parsedOrigin.hostname == undefined || !params.origin.startsWith("https://")) {
|
||||||
this.logService?.warning(`[Fido2Client] Invalid https origin: ${params.origin}`);
|
this.logService?.warning(`[Fido2Client] Invalid https origin: ${params.origin}`);
|
||||||
throw new DOMException("'origin' is not a valid https origin", "SecurityError");
|
throw new DOMException("'origin' is not a valid https origin", "SecurityError");
|
||||||
@@ -211,15 +219,15 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
|||||||
throw new FallbackRequestedError();
|
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 });
|
const parsedOrigin = parse(params.origin, { allowPrivateDomains: true });
|
||||||
params.rpId = params.rpId ?? parsedOrigin.hostname;
|
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://")) {
|
if (parsedOrigin.hostname == undefined || !params.origin.startsWith("https://")) {
|
||||||
this.logService?.warning(`[Fido2Client] Invalid https origin: ${params.origin}`);
|
this.logService?.warning(`[Fido2Client] Invalid https origin: ${params.origin}`);
|
||||||
throw new DOMException("'origin' is not a valid https origin", "SecurityError");
|
throw new DOMException("'origin' is not a valid https origin", "SecurityError");
|
||||||
|
|||||||
Reference in New Issue
Block a user