Where the Credential Lives — XSS and CSRF
AdvancedlocalStorage is exposed to any script on the page; a cookie is sent automatically by any site. Each storage choice picks which attack you have to defend against.
Overview
This is the security decision a full-stack interview will probe, because it has no free option. A token in localStorage is immune to CSRF, since an attacker's site cannot read your storage — but any XSS, including one inside a dependency you did not audit, steals every session. An httpOnly cookie cannot be read by JavaScript at all, so XSS cannot exfiltrate it — but the browser attaches it to cross-site requests automatically, which is CSRF. You do not choose safety; you choose which threat you are equipped to handle.
The Comparison
Four options, and what each exposes.
// localStorage
// readable by any script on the origin — one XSS takes everything
// survives a tab close, so a shared machine keeps the session
// immune to CSRF, easy across origins
localStorage.setItem('token', jwt) // convenient, and exposed
// httpOnly cookie
// JavaScript cannot read it, so XSS cannot exfiltrate it
// sent automatically -> needs SameSite and/or a CSRF token
Set-Cookie: sid=...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400
// In-memory (a JS variable or React state)
// safest against persistence — gone on refresh, which is the point
// needs a refresh cookie to restore the session on reload
// still readable by an XSS while the page is open
// sessionStorage
// per-tab, cleared on close. Same XSS exposure as localStorage.
// The honest summary:
// XSS defeats every client-side storage option. httpOnly only
// stops the token being READ — an XSS can still make requests as
// the user from the page itself.
// So preventing XSS is the primary defence, and storage choice is
// damage limitation.Cookie Attributes
Five attributes, each doing a specific job.
Set-Cookie: sid=abc;
HttpOnly; # JavaScript cannot read it
Secure; # HTTPS only — never omit in production
SameSite=Lax; # not sent on cross-site subrequests
Path=/;
Max-Age=86400; # session cookie if omitted
Domain=.aicancode.org # shared across subdomains — widen carefully
# SameSite decides the CSRF exposure:
# Strict never sent cross-site. Breaks arriving from an external
# link while logged in — the user appears logged out.
# Lax (default) sent on top-level GET navigations only. Blocks
# cross-site POST, which is the CSRF that matters.
# None always sent. REQUIRES Secure. Needed when the API is on a
# genuinely different site — and then you need CSRF tokens.
# The production trap: aicancode.org calling api.fly.dev is
# CROSS-SITE, so a Lax cookie is never sent and auth silently fails
# in production while working perfectly on localhost, where both are
# "localhost". Fix by putting the API on api.aicancode.org — same
# site, so Lax works — or by proxying it through the frontend.Defending Both
What actually stops each attack.
// XSS — the root defence, whatever your storage:
// React escapes interpolated text by default
// dangerouslySetInnerHTML only with DOMPurify.sanitize()
// validate href protocols before rendering (javascript: executes)
// a Content-Security-Policy header, which blocks inline scripts
Content-Security-Policy: default-src 'self'; script-src 'self'
// CSRF — needed whenever the browser attaches credentials for you:
// 1. SameSite=Lax or Strict <- covers most cases now
// 2. a CSRF token for state-changing requests, double-submitted
// via a readable cookie and a header
// 3. verify the Origin header server-side
if request.headers.get("origin") not in ALLOWED_ORIGINS:
raise HTTPException(403)
// Note that a token in an Authorization header needs no CSRF token:
// the browser does not attach it automatically, so an attacker's
// page cannot cause it to be sent.
// A defensible modern default for a browser app:
// httpOnly + Secure + SameSite=Lax session cookie
// API on a subdomain of the same site
// a strict CSP
// short sessions with idle timeoutKey Points to Remember
- 1localStorage is readable by any script, so one XSS — including one in a dependency — takes every session
- 2An httpOnly cookie cannot be read by JavaScript but is sent automatically, which is what CSRF exploits
- 3XSS defeats every storage option, so a Content-Security-Policy and escaping are the primary defence
- 4SameSite=Lax blocks cross-site POSTs; a cross-site API needs SameSite=None with Secure plus CSRF tokens
- 5A cookie that works on localhost can fail in production because two real domains are cross-site while two localhost ports are not
Interview Questions
Sign in to ask AriaWhat are the trade-offs between storing a token in localStorage and in an httpOnly cookie?
Why does a token sent in an Authorization header not need CSRF protection?
Your cookie auth works locally but not in production. What is the likely cause?
Ask Aria about Where the Credential Lives — XSS and CSRF
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.