Home/Learn/Full-Stack Integration/Where the Credential Lives — XSS and CSRF

Where the Credential Lives — XSS and CSRF

Advanced
Auth in the Browser

localStorage 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.

Every option loses to XSS; httpOnly limits the damage
// 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.

SameSite is the CSRF control
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.

CSP for XSS, SameSite plus tokens for CSRF
// 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 timeout

Key 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 Aria
1

What are the trade-offs between storing a token in localStorage and in an httpOnly cookie?

Hard
2

Why does a token sent in an Authorization header not need CSRF protection?

Hard
3

Your cookie auth works locally but not in production. What is the likely cause?

Hard

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.

Loading discussion…