Sensitive-name protection
A speed bump for the obvious mistake: opting
passwordinto client-side storage warns and skips the opt-in. The secret stays unwritten, which is the safe default. PassacknowledgeSensitive: trueif the client-side persistence is intentional.
- Category
- Module
- Default list
DEFAULT_SENSITIVE_NAMES- Override
- { "createAttaform({ defaults": { "sensitiveNames": null } }
Attaform never throws over a recoverable misconfiguration like this. A sensitive-named persist opt-in lands as a one-shot dev warning, the field is dropped from the persist set, and the form mounts cleanly. Devs see the warning during testing and either acknowledge or remove the opt-in; production users never see anything unsafe.
The default list
DEFAULT_SENSITIVE_NAMES covers the common shapes: passwords, card data, identifiers, tokens, MFA artifacts.
password, passwd, pwd, pin, cvv, cvc, ssn, social-security, dob,
date-of-birth, card-number, card, iban, routing-number,
account-number, passport, driver-license, mfa-secret, recovery-code,
token, secret, api-key, private-key
Opting one of them into persistence is a quiet no-op plus a console warning:
<!-- Warns once in dev; the field is not persisted. -->
<input v-register="form.register('password', { persist: true })" />
The match is case-insensitive and ignores separator punctuation: apiKey, api_key, api-key, and API.KEY all hit the same api-key entry. Slash-separated segments and camelCase boundaries split into words before comparison.
Acknowledging per field
When persistence is intentional (custom encrypted adapter, narrow-scope internal tool, server-managed encryption layer), pass acknowledgeSensitive: true:
<input v-register="form.register('password', { persist: true, acknowledgeSensitive: true })" />
The acknowledgement silences the warning for that exact register call and lets the value through to storage. It does NOT remove the path from the resolved sensitive-names list, so the same path stays stripped from multi-tab broadcasts.
Treat acknowledgeSensitive: true as a code-review trigger, not a soundness boundary. The heuristic doesn't catch alias-typed paths (register('pswd' as 'password')), abbreviated variants not in the list, or schemas with deliberately innocuous keys for sensitive data. Per-field opt-in is the real defense; this is a default to lean against.
Extending the list
The default is exposed as DEFAULT_SENSITIVE_NAMES. Compose your own by spreading it:
import { createAttaform, DEFAULT_SENSITIVE_NAMES } from 'attaform'
createAttaform({
defaults: {
sensitiveNames: [...DEFAULT_SENSITIVE_NAMES, 'mrn', 'tax_id', 'health_record'],
},
})
The resolved list applies to every form created by that app. Per-form overrides land via useForm({ sensitiveNames }): the same union type, same matching rules.
One list, two surfaces
The resolved sensitiveNames list gates two write paths:
- Persistence:
{ persist: true }on a sensitive path warns and skips the opt-in. Container opt-ins shed nested sensitive leaves the same way (register('payment', { persist: true })writespayment.last4but notpayment.cvv, even if no one ever directly registeredcvv). The field stays out of storage unless you acknowledge it. - Multi-tab sync: matching paths are stripped outbound (the broadcaster never posts them) AND rejected inbound (receivers drop them even if a peer tries to slip them through).
DevTools renders raw values by design. It is a dev-only surface, and redacting across every place a value surfaces (panels, logs, network tabs, breakpoints, source maps) is impractical security theater, not a real safeguard. Don't share your screen with the DevTools panel open over a customer's session.
Where to next
- Per-field opt-in: the deliberate per-path opt-in this heuristic backstops.
- Storage backends: the form-level gate before the field gate even matters.
- Multi-tab sync: the second subsystem that uses the same resolved list.