Topics
v6.0.0
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.

PatternHow it worksXSS sensitivity
SameSite cookiesBrowser limits cross-site cookie sendLow
Synchronizer tokenServer secret in header; SOP protects readLower
Double-submit cookieCookie value === header valueHigher — XSS reads cookie
Custom header + BearerHeader not auto-sent cross-originN/A for cookie auth

CSRF Attack Mechanics

Definition

  1. Victim is authenticated to bank.example.com
  2. Victim visits evil.com
  3. Attacker’s page triggers POST to bank.example.com/transfer
  4. Browser sends session cookie automatically
  5. 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.

Definition

SameSite controls when browsers attach cookies on cross-site requests.

ValueBehaviorCSRF impact
StrictCookie only on same-site navigationStrongest cut; UX friction on external links
LaxDefault; blocks many cross-site POSTsGood balance for consumer apps
NoneRequires Secure; full cross-site sendNeeds 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.

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, }, }); }
SynchronizerDouble-submit
Server stateSession store or signed cookieStateless string compare
XSS impactToken harder to exploitXSS reads non-HttpOnly cookie
Best forHigh-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-sensitive

Practical 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

Last updated on