Part VII — Client-side storage and encryption
43. Cookies in depth: attributes, HttpOnly/Secure/SameSite
Cookies are the oldest browser storage mechanism and the only one automatically sent with every matching HTTP request — which is exactly what makes them right for session tokens and wrong for anything large or non-auth-related (they add latency to every single request, including ones for images and static assets).
document.cookie = "theme=dark; max-age=31536000; path=/; SameSite=Lax";
// document.cookie is a deceptively simple-looking API for a genuinely
// weird format: reading it returns ALL cookies as one semicolon-joined
// string; writing it sets exactly ONE cookie per call.
Security-relevant attributes, set by the server (never fully trustworthy
if set only from JS, since JS-set cookies can't set HttpOnly on
themselves):
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/
HttpOnly -> JS (document.cookie) CANNOT read this cookie at all — the single
most important XSS mitigation for session tokens: even a full
XSS payload cannot exfiltrate an HttpOnly cookie via JS.
Secure -> only sent over HTTPS, never plaintext HTTP.
SameSite=Strict -> never sent on cross-site requests, even top-level navigation
from an external link (strongest CSRF protection, but
breaks "click an email link and stay logged in" flows).
SameSite=Lax -> sent on top-level cross-site navigation (clicking a link)
but NOT on cross-site subrequests (images, fetches,
iframes) — the modern browser DEFAULT, a reasonable
CSRF/usability balance.
SameSite=None -> sent on all cross-site requests; REQUIRES Secure — needed
for legitimate third-party embeds (payment widgets,
SSO iframes), but is exactly the setting that enables
cross-site tracking, which is why browsers are
progressively restricting third-party cookies entirely.
War story. An app stored its auth token in
localStorage"for easy JS access" instead of anHttpOnlycookie. A single stored-XSS bug in an unrelated comment-rendering feature let an attacker's script readlocalStorage.authTokendirectly and exfiltrate it to an external server — full account takeover from one XSS bug, because nothing stood between the attacker's injected script and the token. AnHttpOnlycookie would have made that specific token structurally unreadable by the same injected script, containing the blast radius to whatever the XSS could do live in the page, not a portable stolen credential.