Storage is part of the threat model
Every browser storage mechanism trades persistence, accessibility, and attack surface. A value in localStorage can survive a browser restart and is readable by JavaScript running on the origin. A session cookie can be protected from script access with HttpOnly, but it is sent automatically with matching requests and needs CSRF defenses. IndexedDB can hold larger structured data, but it is still accessible to origin scripts and can persist longer than a user expects. Choose storage from the sensitivity and lifetime of the data.
Classify client state as public configuration, user preference, cached content, authentication material, or sensitive customer data. Public configuration can live in the bundle. A theme preference can use localStorage. A session credential usually belongs in a secure, HttpOnly cookie or a platform-provided credential store. Sensitive records should be fetched with authorization and cleared according to the product's logout and retention behavior.
Do not put bearer tokens in convenient places
A bearer token in localStorage is available to any script that executes in the page's origin. An XSS bug, compromised dependency, or unsafe HTML injection can read it and send it elsewhere. An HttpOnly cookie reduces direct script access, but the browser attaches it to requests, so the server must validate origin, use SameSite appropriately, and protect state-changing actions against CSRF. No storage choice removes the need for output encoding and Content Security Policy.
If a client must hold a short-lived access token, keep the lifetime and scope narrow and avoid persisting it across restarts. Use a refresh flow designed for the client type, rotate refresh credentials, and revoke sessions when risk changes. Do not place tokens in URLs, page titles, analytics parameters, or error messages. A token copied into a debugging tool should be treated as compromised until rotated.
Make persistence and logout explicit
Users should understand what survives a tab close, a browser restart, and a logout. Document whether a preference is local to one device or synchronized with an account. On logout, clear client caches and revoke or invalidate the server session according to the authentication model. Do not rely on clearing one key when a library may have stored a token under several names or in a service worker cache.
A multi-tab application needs coordination. Broadcast a logout event or listen for storage changes so another tab does not continue to show authenticated data after the session ends. When an account changes, invalidate cached queries tied to the old user. A stale UI is not only confusing; it can expose private information on a shared computer.
Treat cached data as personal data when appropriate
A browser cache can contain customer names, API responses, attachments, and search history even when the application never intentionally stores them. Set cache headers for private responses, avoid persisting sensitive query results by default, and clear service-worker caches on account changes when the product requires it. IndexedDB records need a schema version, retention policy, and deletion path. Do not assume private browsing or a device lock makes retention irrelevant.
If the product supports offline work, model the local database as a replica with explicit encryption, conflict handling, and expiration. A browser storage API does not provide application-level access control between users of the same device profile. Keep the offline dataset minimal and provide a visible way to remove it. Sensitive offline support is a product feature that deserves a threat model, not an accidental side effect of caching.
Prevent storage-based injection and confusion
Never render a stored value as HTML without sanitization and a clear trust boundary. A malicious string saved as a display name or preference can become an XSS payload when another component inserts it unsafely. Validate structured values when reading because users can edit localStorage through developer tools or extensions. Treat all client storage as untrusted input at the server boundary as well.
Use namespaced keys and strict formats. A key called role or plan should not decide server authorization, and a client-side feature flag should not grant access to a paid operation. Include an account or tenant identifier only as a cache partition, not as proof of ownership. The server remains the authority for permissions and entitlements.
Observe without recording what users store
Analytics should report storage errors, migration failures, and cache hit behavior without sending the stored value. Avoid logging full keys when they may include identifiers. A crash report that serializes application state can expose tokens and customer data, so configure redaction and test it. If a diagnostic mode inspects storage, make it explicit and restrict it to the user's device or a redacted export.
The browser's privacy model is stronger when the application knows what it stores. Maintain a small inventory of keys, purpose, lifetime, sensitivity, and clearing behavior. Review it when authentication, analytics, or offline support changes. An inventory turns an invisible retention problem into a concrete engineering checklist.
Test browsers, tabs, and recovery
Test first visit, reload, restart, logout, account switch, multiple tabs, blocked storage, quota exhaustion, private browsing, service-worker updates, and a corrupted value. Verify that a malformed value is ignored or migrated safely, not trusted. Test that a user who logs out in one tab cannot access cached data in another. Include a dependency compromise scenario in security review for any client-side token storage.
Good browser storage is deliberately boring. Keep authentication material out of script-readable persistence where possible, make retention visible, clear data on lifecycle changes, validate every value, and preserve server-side authorization. The browser is a powerful local environment, but it is also a shared and script-extensible one. Design for both convenience and loss of control.
Implementation example
Keep authorization and entitlements server-side; use browser storage only for non-sensitive preferences or bounded caches. Name every key, define its lifetime and owner, and clear account-scoped data on logout or account switch. Treat service workers, IndexedDB, caches, and restored tabs as part of the deletion surface.
localStorage.setItem('ui.theme', 'dark');
// Never store access tokens, recovery codes, or authorization decisions here.Verify and troubleshoot
Test shared browser profiles, private browsing, logout, account switching, tab restore, storage quota errors, browser extensions, and service-worker caches. Inspect keys and payloads with synthetic identities and verify that prohibited fields never reach analytics or crash logs. A client-only feature should still have a reset and migration test.
Operations and recovery
Version storage schemas, invalidate unsafe values on migration, and provide a user-visible reset path. If sensitive data was persisted, expire sessions or credentials as appropriate, ship a cleanup migration, and communicate the scope. Do not rely on clearing local storage as a substitute for server-side revocation.
References and further reading
Use the browser storage API specifications, OWASP HTML5 Security guidance, and the application's privacy and retention policy. Record which data is intentionally client-persistent and why.