# No PII in browser storage (localStorage/sessionStorage); encrypted if needed

- **Pattern:** `ab-000842` (`data-protection.storage-retention.browser-storage-secure`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000842
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000842)

## Why it matters

GDPR Art. 32 requires appropriate security measures for personal data, and storing PII in `localStorage` is a direct failure of that obligation: any JavaScript running on the page — including third-party analytics, ad scripts, or injected malicious code — can read `localStorage` in full. OWASP A02 (Cryptographic Failures) explicitly calls out sensitive data exposure via client-side storage as a top vulnerability. CWE-522 covers insufficiently protected credentials, which includes JWTs containing PII stored where XSS attacks can exfiltrate them. ISO-27001:2022 A.8.24 requires cryptographic protection of data at rest — but `localStorage` provides no encryption at all. If a user's email, name, or session token is in `localStorage`, a single XSS payload harvests it silently.

## Severity rationale

Medium because exploitation requires a co-located XSS or malicious script, but that is a realistic threat given the volume of third-party analytics and tracking scripts most apps load.

## Remediation

Move authentication tokens from `localStorage` to `HttpOnly` cookies set server-side. HttpOnly cookies are inaccessible to JavaScript by specification, eliminating the XSS exfiltration vector.

```ts
// app/api/auth/login/route.ts (Next.js)
import { cookies } from 'next/headers'

export async function POST(req: Request) {
  const { email, password } = await req.json()
  const token = await authenticateUser(email, password)

  cookies().set('session', token, {
    httpOnly: true,   // inaccessible to JavaScript
    secure: true,     // HTTPS only
    sameSite: 'lax',  // CSRF protection
    maxAge: 60 * 60 * 24 * 7,
    path: '/',
  })

  return Response.json({ ok: true })
}
```

Audit all `localStorage.setItem` calls in the codebase. For each one, verify no PII (email, phone, full name, address) is stored. User preferences (theme, language) are safe to store in `localStorage` — contact information is not.

## Detection

- **ID:** `browser-storage-secure`
- **Severity:** `medium`
- **What to look for:** Enumerate every relevant item. Search the codebase for all calls to `localStorage.setItem` and `sessionStorage.setItem`. For each call, inspect what data is stored. Common violations: storing the full user object (which includes email, name, phone), storing JWT tokens that contain PII in their payload, storing form state that includes personal data. Also check IndexedDB usage. Verify that authentication tokens are handled via HttpOnly cookies (set server-side via `Set-Cookie`) rather than localStorage. Check if any browser-extension or third-party library is storing PII in localStorage on the application's behalf.
- **Pass criteria:** At least 1 of the following conditions is met. No personal data (email, phone, address, SSN, full name) is stored in plaintext in localStorage or sessionStorage. Authentication tokens use HttpOnly, Secure, SameSite cookies. If any data must be in browser storage (e.g., user preferences that include username), it is either non-sensitive or encrypted.
- **Fail criteria:** User email, phone number, full name, or other PII stored in plaintext in localStorage or sessionStorage. JWT tokens that include PII stored in localStorage (accessible to any JavaScript, including third-party scripts).
- **Skip (N/A) when:** Application does not use browser storage at all and uses only server-side sessions.
- **Detail on fail:** Example: `"localStorage stores the full user object including email and phone at key 'current_user'. Accessible to all JavaScript on the page."` or `"JWT containing user email and name stored in localStorage.getItem('token') rather than HttpOnly cookie."`.
- **Remediation:** Move authentication tokens to HttpOnly cookies and remove PII from browser storage:

  ```ts
  // BEFORE — wrong: JWT in localStorage
  // localStorage.setItem('token', jwtToken)  // DO NOT DO THIS

  // AFTER — correct: HttpOnly cookie set server-side (Next.js API route example)
  // app/api/auth/login/route.ts
  import { cookies } from 'next/headers'

  export async function POST(req: Request) {
    const { email, password } = await req.json()
    const token = await authenticateUser(email, password)

    cookies().set('session', token, {
      httpOnly: true,   // not accessible to JavaScript
      secure: true,     // HTTPS only
      sameSite: 'lax',  // CSRF protection
      maxAge: 60 * 60 * 24 * 7, // 7 days
      path: '/',
    })

    return Response.json({ ok: true })
  }
  ```

  For user preferences that need to persist client-side (theme, language), store only non-PII values. If you need to cache the user's display name for UI purposes, prefer `sessionStorage` over `localStorage` (clears on tab close) and store only non-sensitive display data, not contact information.

---

## External references

- cwe CWE-312
- cwe CWE-522
- owasp A02
- gdpr Art. 32
- iso-27001 A.8.24

Taxons: privacy-consent, cryptography-and-secrets

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