Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions extensions/dsh-browser/src/content/privacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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
Expand Down
41 changes: 41 additions & 0 deletions extensions/dsh-browser/tests/privacy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading