CMMC 2.0 MP.L1-3.8.3 (NIST 800-171r2 3.8.3) covers controlled access to sensitive data. Storing authentication tokens or JWT strings in localStorage or sessionStorage makes them accessible to any JavaScript executing on the page — including injected third-party scripts and XSS payloads. Unlike HttpOnly cookies, browser storage offers no protection against script-based exfiltration. CWE-922 (Insecure Storage of Sensitive Information) and OWASP A02 (Cryptographic Failures) both apply. A token stored in localStorage that encodes user role and email exposes the user's identity and access level to any script that runs in the same origin.
Medium because exploiting browser storage requires XSS or a malicious script on the same origin, but when that occurs, all stored tokens are immediately readable with no additional access barrier.
Deliver authentication tokens as HttpOnly cookies set by the server — client code never touches the token value. For UI state that requires user info (display name, role label), fetch it from a dedicated endpoint rather than caching sensitive data in browser storage:
// DO NOT do this:
// localStorage.setItem('auth_token', response.token)
// Instead: server sets the cookie on login response
// Set-Cookie: session=<token>; HttpOnly; Secure; SameSite=Lax
// For UI-only user info:
const [user, setUser] = useState<User | null>(null)
useEffect(() => {
fetch('/api/auth/me').then(r => r.ok ? r.json() : null).then(setUser)
}, [])
Audit all localStorage.setItem and sessionStorage.setItem calls in the codebase — non-sensitive UI preferences (theme, sidebar state) are acceptable; anything that identifies the user, their role, or their access credentials is not. Cross-reference with the authentication-verify check to confirm the session cookie flags are correctly set.
ID: gov-cmmc-level-1.media-protection.browser-storage
Severity: medium
CMMC Practice: Derived from MP.L1-3.8.3
What to look for: Search the codebase for localStorage, sessionStorage, and indexedDB usage. Examine what data is being stored in each — look for authentication tokens, session identifiers, user profile data, or any field that could constitute FCI. Check whether auth tokens are stored in browser storage (bad) vs HttpOnly cookies (good). Look for patterns like localStorage.setItem('token', ...), localStorage.setItem('user', JSON.stringify(user)), or any storage of credential-adjacent data. Also check for service worker cache configurations that may persist sensitive responses.
Pass criteria: Count all localStorage, sessionStorage, and IndexedDB calls in client-side code. No more than 0 authentication tokens or session identifiers may be in browser storage. Tokens delivered via HttpOnly cookies only. Report: "X browser storage calls found, 0 contain sensitive data."
Cross-reference: For authentication mechanism security (hashing, sessions), see the authentication-verify check in this audit.
Fail criteria: Authentication tokens or JWT strings stored in localStorage. Sensitive user data (email, role, permissions, contract identifiers) stored in sessionStorage or localStorage in plaintext. Session identifiers accessible to JavaScript.
Skip (N/A) when: Application uses no client-side storage (no localStorage, sessionStorage, or IndexedDB calls anywhere in the codebase).
Detail on fail: Specify what sensitive data is stored and where. Example: "components/auth/login.tsx: localStorage.setItem('auth_token', token) after login. JWT contains user role and email — accessible to any JavaScript on the page." Keep under 500 characters.
Remediation: Move authentication tokens to HttpOnly cookies and avoid storing sensitive data in browser storage:
// WRONG — never store auth tokens in localStorage
// localStorage.setItem('auth_token', response.token)
// CORRECT — tokens should arrive as HttpOnly cookies set by the server
// Server sets: Set-Cookie: session=<token>; HttpOnly; Secure; SameSite=Lax
// Client code never touches the token directly
// If you need client-accessible user info (e.g., display name for UI rendering),
// fetch it from a dedicated endpoint rather than storing in browser storage:
const [user, setUser] = useState<User | null>(null)
useEffect(() => {
fetch('/api/auth/me')
.then(r => r.ok ? r.json() : null)
.then(setUser)
}, [])
For any legitimate client-side caching (e.g., UI preferences), store only non-sensitive data:
// Acceptable — UI preferences, non-sensitive settings
localStorage.setItem('theme', 'dark')
localStorage.setItem('sidebar_collapsed', 'true')
// Not acceptable — anything that identifies the user or their access level
// Do NOT store: email, user ID, role, permissions, contract numbers, or tokens