# No sensitive payment data stored in localStorage or sessionStorage

- **Pattern:** `ab-001160` (`ecommerce-payment-security.client-side-handling.no-storage-sensitive-data`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001160
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001160)

## Why it matters

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.

## Severity rationale

High because payment tokens persisted in browser storage are indefinitely accessible to any JavaScript on the page, including XSS payloads and browser extensions.

## Remediation

Use payment tokens immediately and discard them — never persist them to browser storage. Keep in-flight payment state in React component state only.

```ts
// 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.

## Detection

- **ID:** `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:

  ```ts
  // 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.

## External references

- cwe CWE-312
- cwe CWE-922
- owasp A02
- pci-dss Req 3.3

Taxons: cryptography-and-secrets, injection-and-input-trust

HTML version: https://auditbuffet.com/patterns/ab-001160
