On this page · sections
CSRF Protection
Cross-Site Request Forgery (CSRF) forges authenticated requests using the victim’s session cookies. The attacker never reads the cookie — they ride it.
| Pattern | How it works | XSS sensitivity |
|---|---|---|
| SameSite cookies | Browser limits cross-site cookie send | Low |
| Synchronizer token | Server secret in header; SOP protects read | Lower |
| Double-submit cookie | Cookie value === header value | Higher — XSS reads cookie |
| Custom header + Bearer | Header not auto-sent cross-origin | N/A for cookie auth |
CSRF Attack Mechanics
Definition
- Victim is authenticated to
bank.example.com - Victim visits
evil.com - Attacker’s page triggers POST to
bank.example.com/transfer - Browser sends session cookie automatically
- Server processes mutation as authenticated victim
<!-- evil.com — classic form CSRF -->
<form action="https://bank.example.com/transfer" method="POST" id="csrf">
<input type="hidden" name="to" value="attacker-acct" />
<input type="hidden" name="amount" value="10000" />
</form>
<script>
document.getElementById('csrf').submit();
</script>'use strict';
fetch('https://bank.example.com/transfer', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'to=attacker&amount=10000',
});Contrast with XSS: XSS steals credentials; CSRF rides them without reading.
CORS does not block CSRF — CORS blocks reading the response cross-origin; the request may still reach the server and mutate state.
Targets: state-changing endpoints (POST, PUT, DELETE). Never mutate on GET — CSRF via <img src="..."> works without JavaScript.
Interview: CSRF and XSS are independent — cookie auto-send only; no script read required.
SameSite Cookie Attribute
Definition
SameSite controls when browsers attach cookies on cross-site requests.
| Value | Behavior | CSRF impact |
|---|---|---|
| Strict | Cookie only on same-site navigation | Strongest cut; UX friction on external links |
| Lax | Default; blocks many cross-site POSTs | Good balance for consumer apps |
| None | Requires Secure; full cross-site send | Needs CSRF token layer |
'use strict';
// Consumer app session — Lax default
// Set-Cookie: session=abc; HttpOnly; Secure; SameSite=Lax; Path=/
// Cross-site embed SSO — None required
// Set-Cookie: sso=xyz; HttpOnly; Secure; SameSite=None; Path=/Not complete alone — Lax gaps remain (top-level GET navigation with mutations). None requires tokens.
app.example.com vs api.example.com may be cross-site under schemeful same-site rules — align on same registrable domain when possible.
Real-world example
OAuth callback cross-site POST may need SameSite=None on state cookie — use PKCE + short state TTL. Third-party cookie phase-out favors first-party BFF auth.
Interview: SameSite helps but does not replace CSRF tokens — defense in depth.
Synchronizer Token Pattern
Definition
Server generates unpredictable CSRF token bound to session. SPA reads token from meta tag, cookie, or /csrf endpoint and sends on POST/PUT/PATCH/DELETE via X-CSRF-Token header.
Attacker on evil.com cannot read token — SOP blocks cross-origin read.
'use client';
function getCsrfToken(): string {
const meta = document.querySelector('meta[name="csrf-token"]');
if (!meta) throw new Error('CSRF meta missing');
return meta.getAttribute('content') ?? '';
}
export async function apiMutate(
path: string,
body: unknown,
method = 'POST',
) {
const res = await fetch(path, {
method,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken(),
},
body: JSON.stringify(body),
});
if (res.status === 403) throw new Error('CSRF validation failed');
return res.json();
}
// Layout injects: <meta name="csrf-token" content={serverToken} />Custom header triggers CORS preflight on cross-origin requests — extra CSRF barrier layered with token validation.
Token in URL query leaks via Referer — use header or body field only. Rotate on login, logout, and privilege change.
Interview: Central fetch wrapper — one bare fetch bypasses all protection.
Double-Submit Cookie Pattern
Definition
Server sets CSRF value in a non-HttpOnly cookie. JavaScript reads the cookie and sends the same value in X-CSRF-Token header. Server compares cookie === header.
'use client';
function readCookie(name: string): string | null {
const match = document.cookie.match(
new RegExp(`(?:^|; )${name}=([^;]*)`),
);
return match ? decodeURIComponent(match[1]) : null;
}
export async function doubleSubmitFetch(
path: string,
init: RequestInit = {},
) {
const csrf = readCookie('csrf_token');
if (!csrf) throw new Error('CSRF cookie missing');
return fetch(path, {
...init,
credentials: 'include',
headers: {
...init.headers,
'X-CSRF-Token': csrf,
},
});
}| Synchronizer | Double-submit | |
|---|---|---|
| Server state | Session store or signed cookie | Stateless string compare |
| XSS impact | Token harder to exploit | XSS reads non-HttpOnly cookie |
| Best for | High-security (banking) | Internal tools with strict subdomain control |
Mitigations for double-submit: __Host- cookie prefix, SameSite=Lax on session cookie, short token TTL.
Interview: Double-submit with HttpOnly CSRF cookie is broken — JS must read the cookie.
Frontend CSRF Responsibilities
Definition
Central fetch wrapper — every mutation via shared client. ESLint rule blocks bare fetch with credentials: 'include' outside wrapper.
'use client';
import { createContext, useContext, useMemo } from 'react';
type ApiClient = {
mutate: (path: string, body: unknown, method?: string) => Promise<unknown>;
};
const ApiContext = createContext<ApiClient | null>(null);
export function ApiProvider({
csrfToken,
children,
}: {
csrfToken: string;
children: React.ReactNode;
}) {
const client = useMemo<ApiClient>(
() => ({
mutate: async (path, body, method = 'POST') => {
const res = await fetch(path, {
method,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
}),
[csrfToken],
);
return (
<ApiContext.Provider value={client}>{children}</ApiContext.Provider>
);
}SPA checklist:
- Every mutation via central fetch with CSRF header
credentials: 'include'only to trusted origins- No sensitive GET actions
- Cross-origin API: BFF proxy or token endpoint with strict CORS
- OAuth state parameter — CSRF protection for login flow itself
- Integration test: forge request without token → 403
Real-world example
JWT in Authorization header — CSRF less relevant; clarify auth mode. Mixed cookie + JWT needs both XSS and CSRF models. GraphQL mutations over cookie auth need token on every request.
Interview: One bare fetch voids all CSRF protection — governance via shared client package and CI forge tests.
Senior Interview Summary
Mental model
CSRF → Forges authenticated request; rides cookies; never reads them
vs XSS → XSS steals credentials; CSRF rides without reading
SameSite → Browser-controlled cross-site cookie send policy
Synchronizer → Server secret in meta; X-CSRF-Token header
Double-submit → Cookie === header; stateless; XSS-sensitivePractical rules
- BFF pattern for same-origin mutations with server-injected token
- Document SameSite expectations with backend
- Multipart file upload needs CSRF token too
- Rate-limit CSRF failures — reconnaissance signal
- Micro-frontends import shared client — not reimplement CSRF
What to say in interviews
No mutation path ships without the shared API client. CI runs integration test that forges cross-origin POST without token and expects 403. We document BFF vs direct API layout and token source. Bearer-only APIs shift threat to XSS — mixed auth needs both models.
Common traps
- CORS blocks reading response — request may still mutate server state
- JSON API with cookie auth needs CSRF token on every mutation
- CSRF token in URL query leaks via Referer
- JWT in header does not mean skip CSRF section when cookies are also used