Suggest an editImprove this articleRefine the answer for “What is CSRF and how to prevent it?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**CSRF (Cross-Site Request Forgery)** is an attack where a malicious site forces a user's browser to send authenticated requests to another site. Browsers send cookies automatically cross-site, so servers cannot tell forged requests from real ones. Fix: add CSRF tokens to state-changing endpoints and set `SameSite=Strict` on session cookies.Shown above the full answer for quick recall.Answer (EN)Image**CSRF (Cross-Site Request Forgery)** is an attack where a malicious site forces a user's browser to send authenticated requests to another site without the user knowing. ## Theory ### TL;DR - Browsers send cookies automatically to any site that set them, regardless of where the request originates - Analogy: a butler who hands your house keys to anyone who asks, because they already know your address - The server sees valid session cookies and has no way to tell if the request came from your form or an attacker's page on `evil.com` - Fix: attach a unique secret token to every state-changing request that attackers cannot read or guess cross-site - `SameSite=Strict` on the session cookie blocks most modern CSRF attacks without any extra code ### Quick Example ```javascript // Vulnerable: no CSRF check - attacker on evil.com can forge this POST app.post('/transfer', (req, res) => { const { to, amount } = req.body; // req.session.user is set via cookie - browser sends it automatically transferMoney(req.session.user.id, to, amount); // runs for forged requests too res.send('Transfer complete'); }); // Protected: csurf middleware blocks requests without a valid token const csurf = require('csurf'); app.use(csurf({ cookie: true })); app.post('/transfer', (req, res) => { // Missing or wrong _csrf token -> 403 Forbidden, request never reaches here transferMoney(req.session.user.id, req.body.to, req.body.amount); res.send('Transfer complete'); }); ``` The difference is one middleware line. Without it, any page on the internet can POST to your endpoint and ride along with the user's session cookie. ### How the Attack Works Browsers follow the Same-Origin Policy for reading data, but cookies are exempt from it. When a browser visits `evil.com` and that page contains `<img src="https://bank.com/transfer?to=attacker&amount=1000">`, the browser fires that GET request and attaches the `bank.com` session cookie automatically. The server sees a valid session and processes it. POST attacks are slightly more involved but equally effective: ```html <!-- On evil.com --> <form id="f" action="https://bank.com/transfer" method="POST"> <input type="hidden" name="to" value="attacker" /> <input type="hidden" name="amount" value="1000" /> </form> <script>document.getElementById('f').submit();</script> <!-- User sees nothing. Transfer goes through. --> ``` The user opens the page, the form submits in the background, and the money moves. The server has no signal that the form came from `evil.com` rather than from `bank.com`. ### When to Apply CSRF Protection - State-changing operations (POST, PUT, DELETE) on authenticated endpoints: always require a token - JSON APIs: `Content-Type: application/json` alone does not protect you (see common mistakes below) - Read-only GET endpoints: no CSRF protection needed, but make sure they really are read-only - Single-page apps: combine `SameSite=Strict` cookies with a custom header like `X-CSRF-Token` - Microservices with JWT: store the token in `Authorization` header, not a cookie, and the CSRF surface disappears entirely ### Prevention Methods **CSRF tokens** are the most widely used approach. The server generates a random value tied to the session and embeds it in every form. On submission it checks whether the value matches. An attacker on `evil.com` cannot read this value because of the Same-Origin Policy. ```javascript // Express + csurf (csurf v1.11.0) const csurf = require('csurf'); app.use(csurf({ cookie: true })); // Expose token for React SPA app.get('/csrf', (req, res) => res.json({ token: req.csrfToken() })); // React form that fetches the token on mount function ProfileForm() { const [csrfToken, setCsrfToken] = useState(''); useEffect(() => { fetch('/csrf').then(r => r.json()).then(d => setCsrfToken(d.token)); }, []); return ( <form action="/profile" method="POST"> <input type="hidden" name="_csrf" value={csrfToken} /> <input name="email" /> <button>Update</button> </form> ); // Forged POST from evil.com -> 403, token is missing } ``` **SameSite cookies** are the modern complement. Set `sameSite: 'strict'` on your session cookie and the browser will not send it on cross-site requests at all. ```javascript res.cookie('sessionId', id, { httpOnly: true, secure: true, sameSite: 'strict' }); ``` `Strict` blocks all cross-site cookie sending. `Lax` allows cookies on top-level GET navigation (a user clicking a link) but blocks cross-site POST. `None` sends cookies everywhere and requires `Secure`. Teams I've worked with often stop at SameSite and skip CSRF tokens. On modern Chrome that mostly holds. But Apple did not fully fix their SameSite implementation until Safari 16.4, and a token-based check costs about ten minutes to add. Using both together is the safe default. **Double-submit cookie** is useful when you have no server-side session store. Generate a random token, put it in a cookie, and also require it in a request header or form field. The server checks they match. An attacker cannot read your cookie from another origin, so they cannot supply both values at once. ### Common Mistakes **Protecting forms but ignoring AJAX endpoints.** `fetch()` with `Content-Type: application/json` does not automatically block CSRF. Browsers permit cross-site JSON POSTs in some configurations. ```javascript // Attacker on evil.com fetch('https://bank.com/api/transfer', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ to: 'attacker', amount: 1000 }) }); // If the server accepts JSON without checking X-CSRF-Token - this succeeds ``` Fix: require `X-CSRF-Token` in headers for all API routes, not just HTML form submissions. **Reusing static tokens.** A token is only useful if it is unpredictable and tied to the session. A hardcoded string like `'static-token-123'` can be stolen via XSS or brute-forced. Use `req.csrfToken()`, which generates a new value per call and binds it to the session. **Using GET for state changes.** Any `<img>` tag can fire a GET request cross-site without any user interaction. ```javascript // Wrong: state change via GET app.get('/user/delete', (req, res) => deleteUser(req.session.userId)); // Correct: POST with CSRF token app.post('/user/delete', csrfProtection, (req, res) => deleteUser(req.session.userId)); ``` **Trusting Origin/Referer headers as the sole defense.** Proxies and privacy tools strip these headers. Some browsers do not send them. OWASP lists this as unreliable on its own. Use it as a secondary check layered on top of tokens, not as the primary one. **Setting `SameSite=Lax` everywhere and assuming you are safe.** `Lax` still allows cookies on top-level POST navigation, which means a malicious link can trigger CSRF in some configurations. ### Real-World Usage - Express.js: `csurf` middleware, used in KeystoneJS and 1M+ npm projects - Django: `@csrf_protect` decorator, enabled globally by default (powers Instagram's admin) - Rails: `protect_from_forgery`, built in (GitHub, Shopify use it) - Spring Boot: `@EnableWebSecurity` with `CsrfTokenRepository` - Next.js: `next-csrf` for API routes in Vercel templates For microservices with JWT: store the token in the `Authorization` header. Browsers do not attach custom headers to cross-site requests automatically, so the CSRF attack surface does not exist. ### Follow-up Questions **Q:** What is the difference between CSRF and XSS? **A:** XSS injects malicious code into your site and can steal tokens directly. CSRF uses the user's existing authentication from another origin without injecting anything. XSS breaks the Same-Origin Policy. CSRF exploits the cookie exception to it. **Q:** How does `SameSite=Strict` prevent CSRF? **A:** The browser does not send the session cookie on any cross-site request, including forged POST forms. Without the cookie the server sees an unauthenticated request and rejects it. Chrome added `SameSite=Lax` as the default in version 80. **Q:** What is the double-submit cookie technique? **A:** Generate a random token, put it in a cookie, and also require it in a request header or form field. The server checks both values match. An attacker cannot read your cookie from another origin, so they cannot provide both at the same time. **Q:** Why not just verify Origin and Referer headers? **A:** Proxies and privacy tools strip these headers, and some browsers do not send them. OWASP marks this as an unreliable standalone defense. Use it as a secondary layer on top of token validation. **Q:** In a microservices setup with JWT, how do you handle CSRF at the API gateway? **A:** Store the JWT in the `Authorization` header, not a cookie. Browsers do not automatically attach custom headers to cross-site requests, so there is no CSRF vector. If you must use cookies for the JWT, add a signed `X-CSRF-Token` header and validate it per service to close the gap. ## Examples ### Basic: What the Attack Looks Like ```html <!-- evil.com - silent auto-submit on page load --> <form id="f" action="https://bank.com/transfer" method="POST"> <input type="hidden" name="to" value="attacker@evil.com" /> <input type="hidden" name="amount" value="5000" /> </form> <script>document.getElementById('f').submit();</script> <!-- User sees a blank page for half a second. Transfer is already done. --> ``` If `bank.com` does not validate a CSRF token, the transfer completes. The user's browser did exactly what it was instructed to do, with full authentication attached. ### Intermediate: Token-Protected Form in React ```jsx // Client: fetches CSRF token from the server before rendering function TransferForm() { const [token, setToken] = useState(''); const [to, setTo] = useState(''); const [amount, setAmount] = useState(''); useEffect(() => { fetch('/csrf').then(r => r.json()).then(d => setToken(d.token)); }, []); const handleSubmit = async (e) => { e.preventDefault(); await fetch('/api/transfer', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': token // attacker on evil.com cannot read this value }, body: JSON.stringify({ to, amount }) }); }; return ( <form onSubmit={handleSubmit}> <input value={to} onChange={e => setTo(e.target.value)} placeholder="Recipient" /> <input value={amount} onChange={e => setAmount(e.target.value)} placeholder="Amount" /> <button type="submit">Transfer</button> </form> ); } ``` ```javascript // Server: exposes token endpoint, validates on every POST app.get('/csrf', csrfProtection, (req, res) => res.json({ token: req.csrfToken() })); app.post('/api/transfer', csrfProtection, (req, res) => { transferMoney(req.session.user.id, req.body.to, req.body.amount); res.json({ status: 'ok' }); // Returns 403 if X-CSRF-Token is absent or does not match the session token }); ``` ### Advanced: Three-Layer Protection in Express ```javascript const express = require('express'); const csurf = require('csurf'); const cookieParser = require('cookie-parser'); const app = express(); app.use(express.json()); app.use(cookieParser()); // Layer 1: SameSite session cookie - blocks most browser-based forgeries app.use((req, res, next) => { res.cookie('sessionId', req.sessionID, { httpOnly: true, secure: true, sameSite: 'strict' }); next(); }); // Layer 2: CSRF token middleware - catches what SameSite misses const csrfProtection = csurf({ cookie: true }); // Layer 3: Origin check as a secondary guard const checkOrigin = (req, res, next) => { const origin = req.headers.origin; if (origin && !['https://myapp.com'].includes(origin)) { return res.status(403).json({ error: 'Origin not allowed' }); } next(); }; // All three layers active on the sensitive route app.post('/api/transfer', checkOrigin, csrfProtection, (req, res) => { transferMoney(req.session.user.id, req.body.to, req.body.amount); res.json({ status: 'ok' }); }); // Token endpoint for the SPA app.get('/csrf', csrfProtection, (req, res) => { res.json({ token: req.csrfToken() }); }); ``` To get past this setup an attacker would need to bypass SameSite restrictions, guess the CSRF token, and spoof the Origin header simultaneously. Getting past the first two alone is not realistic.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.