From d531f2f57e9fbe90c3f30286592aa1d70b04e0bd Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Mon, 20 Oct 2025 18:14:03 -0400 Subject: [PATCH] support url path matching for targeting rules --- .../autofill-targeting-rules.component.ts | 63 +++++++++---------- .../services/domain-settings.service.ts | 43 +++++++++++-- 2 files changed, 67 insertions(+), 39 deletions(-) diff --git a/apps/browser/src/autofill/popup/settings/autofill-targeting-rules.component.ts b/apps/browser/src/autofill/popup/settings/autofill-targeting-rules.component.ts index 89982aca938..e8cc08b0908 100644 --- a/apps/browser/src/autofill/popup/settings/autofill-targeting-rules.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill-targeting-rules.component.ts @@ -21,7 +21,6 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { AutofillTargetingRulesByDomain } from "@bitwarden/common/autofill/types"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ButtonModule, CardComponent, @@ -185,46 +184,44 @@ export class AutofillTargetingRulesComponent implements AfterViewInit, OnDestroy const formGroup = control as FormGroup; const domain = formGroup.get("domain")?.value; - if (domain && domain !== "") { - const validatedHost = Utils.getHostname(domain); + const normalizedURI = this.domainSettingsService.normalizeAutofillTargetingURI(domain); - if (!validatedHost) { - this.toastService.showToast({ - message: this.i18nService.t("blockedDomainsInvalidDomain", domain), - title: "", - variant: "error", - }); - return; - } + if (!normalizedURI) { + this.toastService.showToast({ + message: this.i18nService.t("blockedDomainsInvalidDomain", domain), + title: "", + variant: "error", + }); + return; + } - const enteredUsername = formGroup.get("username")?.value; - const enteredPassword = formGroup.get("password")?.value; - const enteredTotp = formGroup.get("totp")?.value; + const enteredUsername = formGroup.get("username")?.value; + const enteredPassword = formGroup.get("password")?.value; + const enteredTotp = formGroup.get("totp")?.value; - if (!enteredUsername && !enteredPassword && !enteredTotp) { - this.toastService.showToast({ - message: "No targeting rules were specified for the URL", - title: "", - variant: "error", - }); + if (!enteredUsername && !enteredPassword && !enteredTotp) { + this.toastService.showToast({ + message: "No targeting rules were specified for the URL", + title: "", + variant: "error", + }); - return; - } + return; + } - newUriTargetingRulesSaveState[validatedHost] = {}; + newUriTargetingRulesSaveState[normalizedURI] = {}; - // Only add the property to the object if it has a value - if (enteredUsername) { - newUriTargetingRulesSaveState[validatedHost].username = enteredUsername; - } + // Only add the property to the object if it has a value + if (enteredUsername) { + newUriTargetingRulesSaveState[normalizedURI].username = enteredUsername; + } - if (enteredPassword) { - newUriTargetingRulesSaveState[validatedHost].password = enteredPassword; - } + if (enteredPassword) { + newUriTargetingRulesSaveState[normalizedURI].password = enteredPassword; + } - if (enteredTotp) { - newUriTargetingRulesSaveState[validatedHost].totp = enteredTotp; - } + if (enteredTotp) { + newUriTargetingRulesSaveState[normalizedURI].totp = enteredTotp; } }); diff --git a/libs/common/src/autofill/services/domain-settings.service.ts b/libs/common/src/autofill/services/domain-settings.service.ts index 66a93fff2d8..ef7c226f907 100644 --- a/libs/common/src/autofill/services/domain-settings.service.ts +++ b/libs/common/src/autofill/services/domain-settings.service.ts @@ -123,6 +123,9 @@ export abstract class DomainSettingsService { * Helper function for the common resolution of a given URL against equivalent domains */ getUrlEquivalentDomains: (url: string) => Observable>; + + /** URI normalization for Autofill Targeting rules URLs to ensure state is doing safe, consistent internal comparisons */ + normalizeAutofillTargetingURI: (url: chrome.tabs.Tab["url"]) => chrome.tabs.Tab["url"] | null; } export class DefaultDomainSettingsService implements DomainSettingsService { @@ -205,7 +208,19 @@ export class DefaultDomainSettingsService implements DomainSettingsService { } async setAutofillTargetingRules(newValue: AutofillTargetingRulesByDomain): Promise { - await this.autofillTargetingRulesState.update(() => newValue); + await this.autofillTargetingRulesState.update(() => { + const validatedNewValue = Object.keys(newValue || {}).reduce((updatingValue, valueKey) => { + const normalizedURI = this.normalizeAutofillTargetingURI(valueKey); + + if (normalizedURI) { + return { ...updatingValue, [normalizedURI]: newValue[valueKey] }; + } + + return updatingValue; + }, {}); + + return validatedNewValue; + }); } async setShowFavicons(newValue: boolean): Promise { @@ -245,17 +260,33 @@ export class DefaultDomainSettingsService implements DomainSettingsService { return domains$; } - getUrlAutofillTargetingRules$(url: string): Observable { + getUrlAutofillTargetingRules$(url: chrome.tabs.Tab["url"]): Observable { + const normalizedURI = this.normalizeAutofillTargetingURI(url); + return this.autofillTargetingRules$.pipe( map((autofillTargetingRules) => { - const domain = Utils.getHostname(url); - - if (domain == null) { + if (!normalizedURI) { return {}; } - return autofillTargetingRules?.[domain] || {}; + return autofillTargetingRules?.[normalizedURI] || {}; }), ); } + + normalizeAutofillTargetingURI(url: chrome.tabs.Tab["url"]) { + if (!Utils.isNullOrWhitespace(url)) { + const uriParts = Utils.getUrl(url); + const validatedHostname = Utils.getHostname(url); + + // Prevent directory traversal from malicious paths + const pathParts = uriParts.pathname.split("?"); + const normalizedURI = + uriParts.protocol + "//" + validatedHostname + Utils.normalizePath(pathParts[0]); + + return normalizedURI; + } + + return null; + } }