iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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=

Test yourself

Q1. The most browser-compatible safe default for sessions is…
Q2. SameSite=None requires…
Q3. For the strongest cross-site protection on sensitive flows pick…

Discussion

Loading…