CSRF
Backend Fundamentals · Backend Security · 10 min read
1. What problem does this solve?
Motivation
When a user is logged into a web application, the browser automatically sends the session cookie with every request to that domain. A malicious site can trick the user’s browser into sending a request to the target site without the user’s knowledge. If the target site trusts the cookie, it will execute the request as if the user intended it. This is called Cross‑Site Request Forgery (CSRF).
What problem exists?
- A logged‑in user can be coerced into performing unwanted actions (e.g., changing an email address, transferring money, deleting data).
- The attacker does not need to know the user’s credentials; they only need the user’s browser to send a request.
Why the problem matters
- Data integrity: Unauthorized state changes can corrupt data.
- Financial loss: In e‑commerce or banking, CSRF can trigger real money transfers.
- Reputation: Users lose trust if their accounts can be manipulated.
What happens if this concept is not used?
- Any authenticated endpoint that performs state changes (POST, PUT, DELETE) becomes vulnerable.
- Attackers can build automated scripts that exploit the vulnerability at scale.
Where this concept appears in real backend systems
- Traditional web apps with session cookies (Express, Django, Rails).
- Single‑page applications (React, Vue) that rely on cookies for authentication.
- APIs that use cookie‑based auth (not JWT or OAuth bearer tokens).
2. Mental model
Analogy
Imagine you have a key that opens your front door. You leave the key on the kitchen counter. A stranger walks in, sees the key, and uses it to enter your house while you’re away. The key is the session cookie; the stranger’s trick is the malicious site that forces your browser to send a request.
Engineering model
- Session cookie – automatically attached to every request to the target domain.
- Same‑origin policy – browsers restrict scripts from other origins from reading the cookie, but they can still send it.
- CSRF protection – a secret value (token) that the server generates and the client must send back with each state‑changing request. The server verifies the token before acting.
3. Deep explanation
| Topic | Explanation |
|---|---|
| Internals | The server generates a random token per user session (or per request). It sends the token to the client in a cookie or in the HTML. The client must include the token in a hidden form field or a custom header (X-CSRF-Token). The server compares the received token with the one it stored. |
| Important terminology | • Synchronizer token pattern – server stores token in session; client submits it back. • Double‑submit cookie – token stored in a cookie and also sent in a request header; no server‑side storage. • SameSite cookie attribute – instructs browsers to omit cookies on cross‑site requests. • Origin / Referer header – can be used as a lightweight CSRF check. |
| Trade‑offs | • Stateless vs stateful: synchronizer pattern requires server‑side storage (session DB). Double‑submit is stateless but vulnerable to replay if not rotated. • Complexity: adding CSRF middleware increases code paths. • Performance: token generation and verification add negligible overhead. |
| Failure modes | • Token missing → request rejected. • Token mismatch → request rejected. • Token expired → user must re‑authenticate. • SameSite disabled → CSRF protection relies solely on tokens. |
| Limitations | • CSRF tokens do not protect GET requests (they should be idempotent). • APIs that use bearer tokens (JWT) are not vulnerable to CSRF because the token is not automatically sent by the browser. • Requires that the client can read the token (e.g., via JavaScript). |
| Where it fits | CSRF protection sits between the HTTP request and the application logic. It is usually implemented as middleware in frameworks (Express, FastAPI, Django). |
| Interaction with related concepts | • SameSite: reduces the need for CSRF tokens but is not a silver bullet. • CORS: Cross‑origin requests can be blocked, but CSRF can still happen via same‑origin requests. • Authentication: CSRF is orthogonal to auth; it protects authenticated state changes. |
4. How it works
- User logs in → server creates a session and sets a session cookie (
sid) and a CSRF token cookie (_csrf). - Client loads a page → JavaScript reads the
_csrfcookie. - Client submits a form or sends an AJAX request → it includes the CSRF token in a hidden field or
X-CSRF-Tokenheader. - Server receives request → middleware reads the token from the request and compares it to the token stored in the session or the cookie.
- If tokens match → request proceeds.
- If tokens mismatch or missing → server responds with
403 Forbidden.
sequenceDiagram
participant U as User
participant B as Browser
participant S as Server
U->>B: GET /login
B->>S: POST /login (credentials)
S->>B: Set-Cookie: sid=...; HttpOnly
S->>B: Set-Cookie: _csrf=abc123; SameSite=Lax
U->>B: GET /dashboard
B->>S: GET /dashboard; Cookie: sid=...
S->>B: Render page with hidden input _csrf=abc123
U->>B: POST /transfer (amount=100)
B->>S: POST /transfer; Cookie: sid=...; X-CSRF-Token: abc123
S->>S: Verify token
S->>B: 200 OK
5. Practical implementation
Below is a minimal Express + TypeScript example using the csurf middleware (synchronizer token pattern). It also shows a double‑submit cookie approach for comparison.
// src/server.ts
import express from 'express';
import cookieParser from 'cookie-parser';
import csrf from 'csurf';
const app = express();
app.use(cookieParser());
// --- Synchronizer token pattern ------------------------------------
const syncCsrfProtection = csrf({
cookie: { key: '_csrf', httpOnly: true, sameSite: 'lax' },
});
app.get('/form', syncCsrfProtection, (req, res) => {
// Render a form that includes the CSRF token
res.send(`
<form method="POST" action="/submit">
<input type="hidden" name="_csrf" value="${req.csrfToken()}">
<input type="text" name="data">
<button type="submit">Send</button>
</form>
`);
});
app.post('/submit', syncCsrfProtection, (req, res) => {
// CSRF token already validated by middleware
res.send(`Received: ${req.body.data}`);
});
// --- Double‑submit cookie pattern ----------------------------------
const doubleSubmitCsrf = (req: express.Request, res: express.Response, next: express.NextFunction) => {
const tokenFromCookie = req.cookies._csrf;
const tokenFromHeader = req.headers['x-csrf-token'] as string | undefined;
if (!tokenFromCookie || !tokenFromHeader || tokenFromCookie !== tokenFromHeader) {
return res.status(403).send('CSRF token missing or invalid');
}
next();
};
app.get('/ds-form', (req, res) => {
// Generate a random token and set it in a cookie
const token = crypto.randomBytes(32).toString('hex');
res.cookie('_csrf', token, { httpOnly: false, sameSite: 'lax' }); // accessible to JS
res.send(`
<form method="POST" action="/ds-submit">
<input type="text" name="data">
<button type="submit">Send</button>
<script>
// Attach token to header on submit
document.querySelector('form').addEventListener('submit', e => {
e.preventDefault();
fetch('/ds-submit', {
method: 'POST',
headers: { 'X-CSRF-Token': '${token}' },
body: new FormData(e.target)
}).then(r => r.text()).then(console.log);
});
</script>
</form>
`);
});
app.post('/ds-submit', doubleSubmitCsrf, (req, res) => {
res.send(`Double‑submit received: ${req.body.data}`);
});
app.listen(3000, () => console.log('Server listening on :3000'));
Key lines explained
csrf({ cookie: { ... } })– tellscsurfto store the token in a cookie instead of the session.req.csrfToken()– generates a new token for the current request.- In the double‑submit example, the token is stored in a cookie that the browser can read (
httpOnly: false) and also sent in a custom header. The server verifies both values match.
6. Production considerations
| Concern | What to watch for | Mitigation |
|---|---|---|
| Scalability | Token generation per request can be expensive if using a database. | Use stateless double‑submit or short‑lived tokens; cache tokens in Redis if needed. |
| Reliability | Token mismatch can happen if the client’s clock is skewed or the cookie is lost. | Provide clear error messages; allow token rotation on login. |
| Security | SameSite=Lax is not enough for all browsers; older browsers ignore it. | Combine SameSite with CSRF tokens; set Secure flag. |
| Observability | CSRF failures can be noisy. | Log 403 responses with request path and user ID; monitor spike patterns. |
| Performance | Token validation adds a few microseconds. | Keep token size small; avoid DB lookups for stateless patterns. |
| Error handling | Returning generic 403 hides whether the token was missing or wrong. | Optionally return 400 Bad Request for missing token; 403 for mismatch. |
| Resource usage | Storing tokens in session DB consumes memory. | Use short TTL (e.g., 30 min) and purge old sessions. |
| Operational concerns | Cookie settings must be consistent across subdomains. | Use Domain=.example.com and SameSite=None; Secure for cross‑subdomain. |
| Failure recovery | Token theft via XSS. | Ensure CSP, XSS protection, and HttpOnly cookies. |
| Deployment | CDN may strip cookies. | Configure CDN to forward cookies to origin. |
7. Common mistakes
| Mistake | Why it’s problematic |
|---|---|
| Using CSRF tokens only on POST, ignoring PUT/DELETE | Those verbs also change state; missing protection leaves holes. |
Disabling SameSite for convenience | Removes a layer of defense; attackers can now send cross‑site requests with cookies. |
| Storing the same CSRF token for all users | Allows replay attacks; any user can use another’s token. |
| Not rotating tokens on login/logout | Stale tokens can be reused by attackers. |
Using HttpOnly cookie for the CSRF token and then trying to read it via JS | The token will never be sent in a header; validation will fail. |
Relying solely on Referer header | Header can be stripped or spoofed; not reliable. |
| Ignoring CSRF in SPAs that use JWT bearer tokens | If the SPA stores JWT in a cookie, CSRF can still occur. |
Setting SameSite=None without Secure | Cookies will be sent over HTTP, exposing them to MITM. |
8. Senior engineer perspective
What changes at scale?
- Token distribution: In a microservice architecture, each service must validate CSRF tokens. Centralizing token generation (e.g., a dedicated auth service) avoids duplication.
- Statelessness: Prefer double‑submit or signed tokens (JWT‑style) to avoid session DB bottlenecks.
- Token revocation: If a user logs out from one device, all tokens should be invalidated. This requires a revocation list or short token lifetimes.
- Cross‑domain: When services span subdomains,
SameSite=None; Secureis mandatory, and CSRF tokens must be propagated securely. - Monitoring: Aggregate CSRF failures per endpoint; a sudden spike may indicate an attack.
- Cost/performance: Token generation is cheap, but storing tokens in Redis or a DB adds cost. Evaluate whether the added security outweighs the operational overhead.
- When NOT to use: Purely stateless APIs that use bearer tokens (OAuth2, JWT) are not vulnerable to CSRF, so CSRF protection is unnecessary there.
Design decisions
| Decision | Trade‑off |
|---|---|
| Synchronizer vs double‑submit | Synchronizer is more secure (server‑side storage) but adds DB traffic; double‑submit is stateless but vulnerable to replay. |
| Token size | Larger tokens are harder to brute‑force but increase cookie size. |
| Token rotation frequency | Frequent rotation reduces risk but increases complexity. |
| SameSite policy | Strict blocks all cross‑site requests (good for security) but breaks legitimate cross‑site integrations. |
9. Interview questions
1BeginnerWhat is CSRF and why is it a problem?
2BeginnerHow does the SameSite cookie attribute help against CSRF?
SameSite=Lax or Strict tells the browser not to send cookies on cross‑site requests, reducing the chance that a forged request carries the session cookie. It is a defense in depth but not sufficient alone.3SeniorExplain the difference between the synchronizer token pattern and the double‑submit cookie pattern. When would you choose one over the other?
- Synchronizer: Server stores a token per session; client must send it back. Requires server‑side storage but protects against replay.
- Double‑submit: Token is stored in a cookie and also sent in a header; no server storage. Stateless but vulnerable to replay if not rotated. Choose synchronizer for high‑security apps; double‑submit for lightweight services or when you want to avoid DB lookups.
4SeniorDescribe how you would implement CSRF protection in a distributed system where multiple services need to validate the same token.
5Scenario/DebuggingA user reports that after logging in, all POST requests to /api/update-profile return 403 Forbidden. You suspect a CSRF issue. What steps would you take to debug and fix the problem?
- Verify that the CSRF cookie (
_csrf) is set and notHttpOnly. - Check that the client includes the token in the request header or form field.
- Inspect the request in the browser dev tools to confirm the header/cookie values.
- Look at server logs for CSRF middleware errors.
- Ensure the SameSite attribute is not causing the cookie to be omitted.
- If using double‑submit, confirm that the cookie and header match.
- If the token is missing, regenerate it on login and ensure the cookie is sent over HTTPS.
10. Hands‑on task
Task: Add CSRF protection to an existing Next.js API route that updates a user’s profile.
- Install
next-csrfor usecsurfwith a custom API route. - Generate a CSRF token on the server and expose it to the client (e.g., via a
/csrf-tokenendpoint). - In the client, fetch the token and include it in the
X-CSRF-Tokenheader when submitting the form. - Verify that a request without the token receives a
403. - Commit the changes and write a short README explaining how the CSRF flow works.
11. Quick revision
- CSRF = Cross‑Site Request Forgery: forged authenticated requests.
- SameSite cookie attribute reduces CSRF but is not sufficient.
- Synchronizer token pattern: server stores token; client submits it back.
- Double‑submit cookie: token in cookie + header; stateless.
- Token must be unique per user/session and rotated on login/logout.
- Validate token before performing any state‑changing operation.
- CSRF does not protect GET requests; they should be idempotent.
- Use
Secureflag on cookies; serve over HTTPS. - Log CSRF failures; monitor for spikes.
- In SPAs, read the token from a cookie or meta tag and send it in a header.
- For JWT bearer token APIs, CSRF is not a concern.