Cross-Site Request Forgery (CSRF) — tricking a browser into acting for you

CSRF doesn't need to steal anything from the victim at all — it exploits the fact that a browser automatically attaches a user's cookies to every request to a site, including ones triggered by a completely different, malicious page the user happens to have open.

Intermediate

4 min read

The mechanism: browsers attach cookies automatically, regardless of which page triggered the request

<!-- On evil-site.com — the victim never has to click anything -->
<img src="https://bank.com/transfer?to=attacker&amount=1000" />
The victim is logged into bank.com in one tab (a valid session cookie
is stored in their browser). They visit evil-site.com in another tab.
The browser loads the <img> tag above, which makes a REAL request to
bank.com/transfer — and the browser AUTOMATICALLY attaches the victim's
bank.com session cookie to that request, exactly as it would for any
legitimate request to bank.com, regardless of which page triggered it

A browser doesn't ask "was this request initiated by a page the user trusts" before attaching cookies — it attaches whatever cookies belong to the target domain, on every request to that domain, triggered from anywhere. evil-site.com never touches the victim's actual cookie value at all (unlike XSS, CSRF doesn't need to read or steal anything) — it just needs to cause the victim's browser to send a request to bank.com, and the browser does the rest, attaching valid authentication automatically.

Why this specifically targets state-changing requests, not just any request

CSRF is dangerous specifically for requests that CHANGE something —
transferring money, changing an email address, deleting an account —
not for requests that only READ data, since the attacker on evil-site.com
generally can't read the RESPONSE to a cross-origin request they triggered
(that's what CORS, covered in this platform's Node.js domain, actually
restricts) — they can only cause the request to be SENT

The attacker triggering a request via an <img> tag or a hidden auto-submitting form can make the victim's browser send an authenticated request, but they generally can't read the response that comes back (that's the browser's same-origin policy, the mechanism this platform's Node.js domain covered under CORS, doing its job correctly). This is exactly why CSRF is a genuine, serious threat specifically for state-changing actions (transfers, deletions, account changes) — the damage happens the moment the request executes on the server, with no need for the attacker to ever see a response at all.

The fix: a CSRF token the attacker's page genuinely cannot obtain

<form action="/transfer" method="POST">
  <input type="hidden" name="csrf_token" value="a1b2c3-random-unpredictable-value" />
  <!-- ...rest of the form -->
</form>
# Server-side: verify the token matches what was issued for THIS user's session
if request.form["csrf_token"] != session["csrf_token"]:
    abort(403)  # request rejected — the token didn't match

A CSRF token is a random, unpredictable value embedded in the legitimate form, tied to the user's own session — the server rejects any state-changing request that doesn't include the correct, matching token. evil-site.com can trigger a request to bank.com/transfer, but it has no way to read the legitimate bank.com page's HTML to extract that page's own CSRF token (again, the same-origin policy prevents cross-origin reading), so any forged request it sends is missing the token — or has a wrong one — and gets rejected.

SameSite cookies: a real, browser-level defense that needs no token at all

Set-Cookie: session=abc123; SameSite=Lax
SameSite=Strict — the cookie is NEVER sent on a cross-site request, period
SameSite=Lax — sent on cross-site TOP-LEVEL navigation (clicking a link),
               but NOT on cross-site requests triggered by images, forms,
               or scripts embedded in another page (exactly CSRF's typical vector)
SameSite=None — sent on every request regardless of origin (the OLD default,
                genuinely dangerous without a CSRF token as a second layer)

The SameSite cookie attribute tells the browser itself when to attach a cookie to a cross-site request — Lax (the modern browser default) blocks exactly the <img>/auto-submitting-form CSRF vector described above, since those aren't top-level navigations, while Strict is even more restrictive, blocking cross-site requests entirely including a user clicking a legitimate external link into the site. This is a genuine, real defense enforced by the browser itself, independent of CSRF tokens — modern applications commonly use both together, SameSite=Lax (or Strict) as a baseline defense and CSRF tokens as defense-in-depth for the cases SameSite alone doesn't fully cover.

Why this doesn't apply the same way to a pure API using bearer tokens

An API where the client sends `Authorization: Bearer <token>` in a header
(not a cookie) is NOT vulnerable to CSRF in the same way — evil-site.com's
<img>/form tricks can't SET a custom Authorization header at all;
only genuine JavaScript making an explicit fetch() call could add one,
and that fetch() would be subject to CORS restrictions in the first place

CSRF's entire mechanism depends on the browser automatically attaching credentials (cookies) to a request the attacker triggers — an API relying on a bearer token sent in a custom header instead of a cookie isn't automatically attached by the browser the same way, since <img>/form-based attacks have no mechanism to set arbitrary headers. This is exactly why CSRF is primarily a cookie-based-session concern, and a genuinely different, real reason some API designs deliberately avoid cookies for authentication in favor of headers.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. Why does CSRF not need to steal any of the victim's data, unlike XSS?

2. How does a CSRF token actually prevent a forged request from succeeding?

3. How does `SameSite=Lax` on a cookie provide a real defense against CSRF?