CSRF, SameSite & anti-CSRF tokens
Cross-site request forgery
Lesson 13 of 20 · 12 min · Module 4: Requests you can trust
Browsers attach cookies to requests automatically. Cross-site request forgery, or CSRF, abuses that: another site makes your browser send a request to a site where you are signed in, and the request arrives with your session.
Start this lesson →Free. No installation: you write and run the code in your browser, and it is checked automatically.
By the end, you’ll be able to…
- Explain how cross-site request forgery works
- Send an anti-CSRF token with every request that changes data
- Combine tokens with SameSite cookies
The idea
Two defences work together. Cookies with SameSite=Lax are not sent with most requests that other sites start. On top of that, sites use an anti-CSRF token: a random value tied to the session, placed in the page, and sent back with every request that changes something. Other sites cannot read your pages, so they cannot know the token, and the server refuses requests without it.
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch('/api/settings', { method: 'POST', headers: { 'X-CSRF-Token': token } });
01The browser is the confused deputy
A page on evil.example can contain a hidden form that posts to bank.example. When it is submitted, the browser adds bank.example’s cookies, so the bank sees a normal signed-in request. The attacker never sees the answer, but the damage is done.
<form action="https://bank.example/transfer" method="post">
<input type="hidden" name="to" value="attacker">
</form>
<script>document.forms[0].submit();</script>
02Tokens prove the request came from your page
The server puts a random token in every page and remembers it with the session. Only your own pages can read it, so a valid token in a header or form field shows that the request started on your site.
03Defence in depth
SameSite=Lax already stops most cross-site POST requests, but older browsers, subdomains you do not control and GET requests that change data still leave gaps. Keep GET requests free of side effects, and use tokens for everything that changes data.
Your turn
postJSON(url, data) so every request it makes includes the header X-CSRF-Token with the token from <meta name="csrf-token">, and never puts the token in the address. In the form’s submit listener, show Your session has expired. Reload the page and try again. when the server answers 403, Something went wrong. Please try again. for other errors, and the answer’s message on success.What the checks look for
- The token from the page travels in a header
- The new email is sent as JSON
- postJSON adds the token for any address
- The token never appears in an address
- Success is announced
- A rejected token gets a helpful message
- Other errors are handled too
Take this with you
Requests that change data carry a secret token that only your own pages know.
Go a little further
Write an api(method, url, data) helper that adds the token to POST, PUT, PATCH and DELETE requests and leaves GET requests unchanged.