Payment tokens (pm_*, tok_*, pi_*) stored in localStorage or sessionStorage are readable by any JavaScript on the page — same-origin scripts, browser extensions, and XSS payloads all have full access. PCI-DSS 4.0 Req 3.3 prohibits storing sensitive authentication data after authorization; CWE-312 (Cleartext Storage of Sensitive Information) and CWE-922 (Insecure Storage of Sensitive Information) both apply. OWASP A02 (Cryptographic Failures) flags browser storage as an insecure persistence layer for payment credentials. Unlike session cookies, localStorage has no expiry and persists across browser restarts, meaning a compromised token can be extracted days or weeks after the initial checkout.
High because payment tokens persisted in browser storage are indefinitely accessible to any JavaScript on the page, including XSS payloads and browser extensions.
Use payment tokens immediately and discard them — never persist them to browser storage. Keep in-flight payment state in React component state only.
// WRONG — token persisted in localStorage
const { paymentMethod } = await stripe.createPaymentMethod({ type: 'card', card: cardElement })
localStorage.setItem('paymentMethodId', paymentMethod.id) // accessible to all scripts
// CORRECT — token used immediately, never stored
const { paymentMethod } = await stripe.createPaymentMethod({ type: 'card', card: cardElement })
await fetch('/api/confirm-payment', {
method: 'POST',
body: JSON.stringify({ paymentMethodId: paymentMethod.id }),
})
// paymentMethod.id is never written to any browser storage
If you need to persist checkout progress across page reloads, store a server-generated session reference in a Secure; HttpOnly cookie — not the payment token itself.
ID: ecommerce-payment-security.client-side-handling.no-storage-sensitive-data
Severity: high
What to look for: Count every localStorage.setItem, sessionStorage.setItem, and document.cookie = call in client-side code. For each call, inspect the key name and the value being stored, and classify it as payment-sensitive or benign. Flag any storage of: payment tokens (values starting with pm_, tok_, pi_), card data fragments, payment method details, or any value derived from the payment flow. Note that storing a session cart ID or order ID is generally acceptable; storing payment credentials or provider tokens persistently in the browser is not.
Pass criteria: No card numbers, expiry dates, CVC codes, payment tokens, or other payment-sensitive data are stored in localStorage or sessionStorage. No more than 0 payment-sensitive values should appear in browser storage calls. Ephemeral payment state held in React state (not persisted) is acceptable. Report even on pass: "Found X storage calls total, 0 contain payment-sensitive data."
Fail criteria: Any payment-sensitive value is written to localStorage or sessionStorage. This includes Stripe PaymentMethod IDs, payment tokens, or any partial card data.
Skip (N/A) when: Never — this check applies to all projects with client-side payment handling regardless of payment provider.
Detail on fail: Name the specific key and the data type stored. Example: "localStorage.setItem('stripeToken', token.id) found in components/checkout/PaymentStep.tsx — payment tokens stored in localStorage are accessible to any script on the page (XSS risk)"
Remediation: Never persist payment tokens or sensitive data to browser storage. Use in-memory state (React state, component variables) for the payment session duration only:
// WRONG — persisting token to localStorage
const { token } = await stripe.createToken(cardElement)
localStorage.setItem('paymentToken', token.id)
// CORRECT — use the token immediately, do not store it
const { paymentMethod } = await stripe.createPaymentMethod({ type: 'card', card: cardElement })
// Immediately send to server — never stored in browser
await confirmPayment(paymentMethod.id)
If you need to resume a partially completed checkout, store a server-side session reference (a session ID, not payment credentials) in a secure, HttpOnly cookie.