How does CSRF actually work, and how do tokens plus SameSite cookies stop it?
CSRF weaponizes the browser's own trust in a logged-in session — the attacker never sees your cookie, they just get your browser to spend it.
LEARN BY DOINGPut your intuition to work3 steps
How does CSRF actually work, and how do tokens plus SameSite cookies stop it?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Why CSRF works at all
Cross-Site Request Forgery exploits one specific fact about browsers: when a browser sends a request to bank.com, it automatically attaches every cookie scoped to bank.com — regardless of which page or origin initiated that request. If you're logged into your bank in one tab and an attacker's page in another tab silently submits a form to bank.com/transfer, your browser happily attaches your session cookie. The bank's server sees a perfectly authenticated request; it has no way to know the user never intended to send it.
This is why CSRF is often misunderstood as "stealing cookies" — it doesn't. The attacker never reads your cookie value at all. They just get your browser, which already holds a valid session, to fire a request on their behalf. That's the entire attack.
#Anatomy of an attack
The victim never sees anything happen — the form can submit via JavaScript on page load, or the "attack" can be as simple as an <img src="https://bank.com/transfer?to=attacker&amount=5000"> if the bank was careless enough to let a GET request mutate state.
#Defense 1: SameSite cookies
The SameSite cookie attribute tells the browser when to attach a cookie on a cross-site request:
SameSite=Strict— never send the cookie on a cross-site request, even when the user clicks a link from another domain into yours. Airtight against CSRF, but breaks the case where a user follows an external link and expects to already be logged in.SameSite=Lax(the modern browser default when unspecified) — sends the cookie on top-level GET navigations (clicking a link) but withholds it on cross-site POSTs, image loads, and iframe loads. This alone blocks the classic form-auto-submit CSRF attack above, since the forged POST won't carry the cookie.SameSite=None— always sent cross-site; requiresSecure. Needed for legitimate cross-site use cases (an embedded widget, a payment iframe) and offers zero CSRF protection on its own.
SameSite=Lax closes most CSRF attacks for free, which is why it's the default in current browsers. But it is not a complete solution: it doesn't cover subdomain takeovers, doesn't help with clients that don't send cookies as the primary credential (some mobile/API clients), and older browsers may not enforce it at all. Treat it as one layer, not the layer.
#Defense 2: the synchronizer token pattern
The classic, still-standard mitigation is a CSRF token: an unpredictable, per-session (or per-request) value that the server embeds in every form it renders, and which it requires back on every state-changing request. Because the token is not a cookie, the attacker's forged cross-site request has no way to know or attach it.
// server: issuing a token bound to the sessionconst crypto = require('crypto');app.get('/transfer-form', (req, res) => {const token = crypto.randomBytes(32).toString('hex');req.session.csrfToken = token; // stored server-side, keyed to this sessionres.render('transfer', { csrfToken: token });});// server: verifying the token on the mutating requestfunction verifyCsrfToken(req, res, next) {const submitted = req.body._csrf;const expected = req.session.csrfToken;if (!submitted || !expected) {return res.status(403).send('Missing CSRF token');}const a = Buffer.from(submitted);const b = Buffer.from(expected);// constant-time comparison: a plain === leaks timing information// proportional to how many leading characters match, which an// attacker can use to guess the token byte by byteif (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {return res.status(403).send('Invalid CSRF token');}next();}app.post('/transfer', verifyCsrfToken, (req, res) => {// token confirmed to match this session — safe to execute the transferperformTransfer(req.body.to, req.body.amount);res.redirect('/dashboard');});
Two implementation details separate a correct CSRF check from a broken one. First, the comparison must be constant-time (crypto.timingSafeEqual, not ===), because a naive string comparison returns faster the sooner it hits a mismatched byte — an attacker measuring response latency across many attempts can reconstruct the token character by character. Second, the token must be tied to the session server-side (or cryptographically derived from the session id), not just handed back and trusted at face value, otherwise an attacker who can plant any token via a cookie-setting vulnerability elsewhere defeats the check entirely.
#Why you generally want both
SameSite=Lax/Strict and CSRF tokens defend against overlapping but not identical threat models. SameSite is a browser-enforced network-layer control that costs you nothing to implement and covers most cases by default. CSRF tokens are an application-layer control that also protects against subdomain-based attacks, misconfigured CORS, and legacy browsers that ignore SameSite. Relying on SameSite alone is reasonable for a modern, cookie-based web app with tight control over subdomains; relying on tokens alone is the safer default when you can't guarantee every client enforces SameSite correctly. Most mature applications ship both, plus a same-origin check on the Origin/Referer header as a cheap third signal.
🧠 Check your understanding
0/2 · 0/2 answered1. Why is CSRF possible even though the attacker never learns the victim's session cookie value?
2. What does SameSite=Lax specifically block that SameSite=None does not?