SameSite Cookies
SameSite cookies tell the browser whether to send a cookie on cross-site requests. Lax (default since Chrome 80) blocks most CSRF; Strict blocks all; None requires Secure and brings the risk back.
Lax / Strict / None tradeoffs
EXAMPLE
// 1) Express session cookie — sensible default
import session from 'express-session';
import ConnectRedis from 'connect-redis';
import Redis from 'ioredis';
app.use(session({
name: 'sid',
secret: process.env.SESSION_SECRET,
store: new ConnectRedis({ client: new Redis() }),
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // not readable by JS
secure: true, // only over HTTPS (REQUIRED for SameSite=None)
sameSite: 'lax', // safest default for most apps
maxAge: 60 * 60 * 24 * 7 * 1000,
path: '/',
domain: '.example.com', // share across subdomains (optional)
},
}));
# 2) Three SameSite values — what each one does
# Strict — cookie NEVER sent on cross-site requests, including top-level navigations.
# Pro: airtight CSRF defence
# Con: user clicks a link to your site from an email and arrives LOGGED OUT
# Use: banking, admin panels, anywhere strict is fine
#
# Lax — cookie sent on top-level navigation (GET) but NOT on cross-site POST / iframe.
# Pro: blocks 99% of CSRF, keeps the email-link UX working
# Con: GET-with-side-effects is still risky if you allow them
# Use: default for most apps (now Chrome / Firefox / Safari default)
#
# None — cookie sent on ALL cross-site requests (must be Secure).
# Pro: works for embedded widgets, OAuth callbacks across origins
# Con: full CSRF risk surface; needs other defences (token, custom header)
# Use: only when truly needed, paired with CSRF tokens
# 3) Cookie + CSRF token (double-submit) for SameSite=None scenarios
app.use((req, res, next) => {
let token = req.cookies['csrf'];
if (!token) {
token = crypto.randomBytes(32).toString('base64url');
res.cookie('csrf', token, {
sameSite: 'none', secure: true, httpOnly: false,
});
}
res.locals.csrf = token;
if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
const sent = req.headers['x-csrf-token'];
if (sent !== token) return res.status(403).json({ error: 'csrf' });
}
next();
});
# 4) Browser defaults (2024+)
# - Chrome 80+ : SameSite=Lax by default if attribute omitted
# - Firefox 96+ : Lax-by-default in private windows; full rollout in progress
# - Safari : Lax by default
# So OMITTING SameSite already gets you Lax in most browsers.
# Always set it explicitly — better future-proofing.
# 5) Common CSRF scenarios — where SameSite helps + where it doesn't
# evil.com → POSTs to victim.com/transfer
# SameSite=Lax : cookie NOT sent → SAFE
# SameSite=None : cookie sent → vulnerable (need token)
# SameSite=Strict: cookie not sent → SAFE
#
# evil.com sets <img src="victim.com/api?destroy=1"> (a GET with side effects)
# SameSite=Lax : top-level GET → cookie sent → vulnerable. DON'T DO GET-with-side-effects.
# SameSite=Strict: GET from iframe → cookie not sent → SAFE
#
# User clicks email link → loads victim.com
# SameSite=Lax : cookie sent on top-level GET → user logged in. UX win.
# SameSite=Strict: cookie NOT sent → user logged out. UX loss.
# 6) Auth tokens in localStorage — NO
# XSS reads localStorage; HttpOnly cookies are immune.
# Always: HttpOnly + Secure + SameSite=Lax for the session cookie.
# 7) Subdomain attacks — SameSite is per SITE not per ORIGIN
# evil.example.com can send cookies to api.example.com if both share base domain.
# Defence: scope cookies to the most specific subdomain that needs them,
# or use a separate auth domain (auth.example.com vs app.example.com).
# 8) OAuth callback edge case — Lax can break form_post response mode
# Solution: switch to query / fragment response, or set SameSite=None on the auth cookie
# (with a CSRF token added).
# 9) Test cookies in DevTools
# Application → Cookies → check SameSite, Secure, HttpOnly columns
# Network → look at Set-Cookie header in responses, Cookie header in requests
# 10) Cheat sheet for the right combination
# Session cookie: HttpOnly, Secure, SameSite=Lax
# CSRF token (cookie): Secure, SameSite=Lax (NOT HttpOnly — JS reads it)
# Tracking pixel cookie: SameSite=None, Secure (required for cross-site)
# Embed-widget cookie: SameSite=None, Secure, + CSRF token for mutations
Why it matters
SameSite=Lax + HttpOnly + Secure on the session cookie is the modern baseline. Reach for CSRF tokens only when you must drop to SameSite=None (embedded widgets, cross-site OAuth flows).
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Modern default: most browsers default cookies to SameSite=Lax. Set-Cookie: sid=…; HttpOnly; Secure; SameSite=Lax // Strict blocks cross-site on ALL methods, even top-level GET — best for sensitive actions. // None requires Secure and re-enables cross-site sending (for SSO / embeds).Try it Yourself »
Exercise
Modern-browser default for new cookies.
Set-Cookie: sid=…; SameSite=
Three letters.
Discussion
Loading…