From ae01c63c2b1bf4a2d274783d78ed6d781de26006 Mon Sep 17 00:00:00 2001 From: Toto Leung Date: Mon, 5 Oct 2026 15:41:29 +0800 Subject: [PATCH] fix(content): mask multi-token cc-* autocomplete values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isSensitiveField matched the whole autocomplete attribute against 'cc-', so spec-valid token lists like 'section-checkout billing cc-number' escaped masking whenever the field's id/name/aria-label was neutral. Tokenize the attribute per the HTML autocomplete grammar (case-insensitive, whitespace-separated) and flag any credit-card or cc-* detail token. Hardening from adversarial review: read the CONTENT attribute, not the IDL property — Chrome's autocomplete getter is limited to known values and returns '' for token lists it cannot parse ('cc-number foo', 'bogus cc-number'), so the IDL silently dropped markup the mask must see. Tests set the content attribute like production markup, and one guard emulates Chrome's blanked getter while the raw attribute carries the tokens; multi-token, mixed-case, and non-cc lists are covered. --- extensions/dsh-browser/src/content/privacy.ts | 23 ++++++++++- extensions/dsh-browser/tests/privacy.spec.ts | 41 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/extensions/dsh-browser/src/content/privacy.ts b/extensions/dsh-browser/src/content/privacy.ts index f5c871629..887b64f97 100644 --- a/extensions/dsh-browser/src/content/privacy.ts +++ b/extensions/dsh-browser/src/content/privacy.ts @@ -21,6 +21,22 @@ const SENSITIVE_PATTERNS = [ /pwd/i, ] +/** + * Whether an `autocomplete` attribute marks the field as payment-related. + * + * The HTML attribute is a case-insensitive space-separated token list, so + * real checkout markup like `section-checkout billing cc-number` must be + * tokenized before matching — a whole-string `startsWith('cc-')` check + * silently misses every multi-token value. + * + * @param autocomplete - raw `autocomplete` attribute value. + * @returns true when any token is `credit-card` or a `cc-*` detail token. + */ +function hasPaymentAutocompleteToken(autocomplete: string): boolean { + const tokens = autocomplete.trim().toLowerCase().split(/\s+/) + return tokens.includes('credit-card') || tokens.some((token) => token.startsWith('cc-')) +} + /** * Whether a form field must never be echoed back to the model. * @param el - the form element (input/select/textarea). @@ -30,8 +46,11 @@ const SENSITIVE_PATTERNS = [ export function isSensitiveField(el: Element): boolean { if (el instanceof HTMLInputElement) { if (el.type === 'password') return true - const autocomplete = String(el.autocomplete) - if (autocomplete === 'credit-card' || autocomplete.startsWith('cc-')) return true + // Read the CONTENT attribute, not the IDL property: Chrome's + // `autocomplete` getter is limited to known values and returns '' for + // token lists it cannot parse ('cc-number foo', 'bogus cc-number'), so + // the IDL would silently drop markup the mask must see. + if (hasPaymentAutocompleteToken(el.getAttribute('autocomplete') ?? '')) return true } const name = el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement ? el.name diff --git a/extensions/dsh-browser/tests/privacy.spec.ts b/extensions/dsh-browser/tests/privacy.spec.ts index f4ecda3a2..c7856de79 100644 --- a/extensions/dsh-browser/tests/privacy.spec.ts +++ b/extensions/dsh-browser/tests/privacy.spec.ts @@ -26,6 +26,47 @@ describe('isSensitiveField', () => { } }) + it('flags the raw attribute when the IDL getter blanks the value (Chrome)', () => { + // Chrome's `input.autocomplete` IDL getter is limited to known values: + // it returns '' for token lists it cannot parse (e.g. 'cc-number foo', + // 'bogus cc-number'). The mask must read the content attribute, or those + // markup variants leak. Emulate the Chrome getter on the instance while + // the content attribute keeps the real value. + for (const value of ['cc-number foo', 'bogus cc-number', 'section-x billing cc-csc extra']) { + const input = document.createElement('input') + input.id = 'field1' + input.setAttribute('autocomplete', value) + Object.defineProperty(input, 'autocomplete', { get: () => '' }) + expect(isSensitiveField(input), value).toBe(true) + } + }) + + it('flags multi-token cc autocomplete values (HTML token lists)', () => { + // Real checkout markup: `autocomplete="section-checkout billing cc-number"`. + // Neutral id so ONLY the autocomplete token can flag the field. Set the + // CONTENT attribute, as production markup does. + for (const value of ['section-checkout billing cc-number', 'shipping cc-csc', 'billing cc-exp']) { + const input = document.createElement('input') + input.id = 'field1' + input.setAttribute('autocomplete', value) + expect(isSensitiveField(input), value).toBe(true) + } + }) + + it('matches cc autocomplete tokens case-insensitively', () => { + const input = document.createElement('input') + input.id = 'field1' + input.setAttribute('autocomplete', 'CC-Number') + expect(isSensitiveField(input)).toBe(true) + }) + + it('leaves multi-token autocomplete without a cc token alone', () => { + const input = document.createElement('input') + input.id = 'field1' + input.setAttribute('autocomplete', 'section-contact billing email') + expect(isSensitiveField(input)).toBe(false) + }) + it('leaves ordinary fields alone', () => { const input = document.createElement('input') input.id = 'email'