Intro
An introduction to CSRF: what the attack is, what it costs the victim, and the four layers of defence that, together, make it structurally impossible. The shape every framework defaults to today.
CSRF from first principles
EXAMPLE
# ===== What CSRF is =====
# Cross-Site Request Forgery: the attacker tricks the victim's browser into
# making a state-changing request to a target site where the victim is signed
# in. Because the browser auto-attaches the session cookie, the target accepts
# the request as if the victim made it deliberately.
#
# Classic example:
# 1) Victim signs in at https://shop.example. A session cookie is set.
# 2) Victim opens https://attacker.example.
# 3) The attacker page contains:
# <img src='https://shop.example/account/email?to=attacker@evil'>
# OR an auto-submitting form for POST.
# 4) Browser sends the request to shop.example with the session cookie.
# 5) shop.example sees an authenticated request and processes it.
#
# Without defences, the victim's email is changed to attacker@evil — and the
# attacker controls password reset for the account.
# ===== Why it matters =====
# CSRF lets an attacker perform actions AS the victim:
# - Change email or password
# - Transfer money
# - Post content
# - Delete accounts
# - Approve permissions
# It is one of the easier classes of bug to exploit and one of the more
# costly to a user. Frameworks default to defences specifically because
# this is so common.
# ===== Layer 1 — HTTP method discipline =====
# GET = read; never mutate state via GET.
# POST/PUT/PATCH/DELETE = state-changing; require CSRF defences.
#
# Why: GET requests can be triggered by <img>, <link>, prefetch, social media
# unfurl bots, even printed QR codes. POST cannot be triggered by an <img>.
# ===== Layer 2 — SameSite cookies =====
# Modern browsers default cookies to SameSite=Lax.
# Lax means the cookie is sent ONLY on:
# - Same-site requests
# - Top-level GET navigations
# A cross-site POST from attacker.example arrives WITHOUT the session
# cookie -> the target sees an anonymous request and rejects it.
#
# Set explicitly anyway:
# Set-Cookie: sid=abc; HttpOnly; Secure; SameSite=Lax
#
# Strict adds protection to top-level navigation, at the cost of 'click link
# in email -> end up signed out'.
# ===== Layer 3 — CSRF token =====
# Server stores a random per-session token. Every state-changing form embeds
# the token in a hidden field; every state-changing fetch sends it as a header.
# Server rejects writes whose token does not match the session.
#
# The attacker's site cannot read the token (browser blocks cross-origin
# reads of HTML), so they cannot forge the request even if SameSite is
# bypassed somehow.
#
# Frameworks ship this by default:
# - Laravel: @csrf in forms + VerifyCsrfToken middleware
# - Rails: csrf_meta_tags + protect_from_forgery
# - Django: {% csrf_token %} + CsrfViewMiddleware
# - Spring: CookieCsrfTokenRepository (auto)
# ===== Layer 4 — Custom required header (bearer APIs) =====
# A JSON API authenticated by Authorization: Bearer is structurally immune to
# CSRF. Browsers do NOT auto-attach a custom header on cross-origin form
# submissions; the attacker's page cannot send a request with the bearer.
#
# This is why mobile-first apps with bearer tokens skip CSRF tokens entirely.
# ===== When CSRF DOES NOT apply =====
# - Public endpoints (no authentication) -> nothing to forge
# - Bearer-token JSON APIs -> no auto-attached creds
# - Webhook endpoints with HMAC signatures -> verify signature; exempt CSRF
# - GET endpoints that truly only read -> safe by definition
# - mTLS-authenticated routes -> client cert is the auth
# ===== Common bugs =====
# 1) GET endpoint that mutates state
# GET /account/email?to=...
# -> change to POST + CSRF token
#
# 2) Cookie set with SameSite=None to fix cross-domain login
# -> token + Origin check
#
# 3) CSRF middleware allow-list that catches more routes than intended
# -> precise regex anchored to one path
#
# 4) CORS with allow_credentials: true + a permissive origin
# -> exact allowed_origins; never '*'
# ===== Self-defence checklist =====
# - Session cookies: HttpOnly + Secure + SameSite=Lax (or Strict)
# - State changes only on POST/PUT/PATCH/DELETE
# - Framework CSRF middleware enabled by default
# - Webhook routes exempt from CSRF middleware, signature verified instead
# - CORS allowed_origins is an exact list
# - Tests: a forged form from another origin returns 401/403
# - Tests: a GET against a state-changing endpoint returns 405 method-not-allowed
# ===== Self-test =====
# Can attacker.example, by JUST loading a page in the victim's browser, do
# any of these on YOUR site?
# - Change email / password
# - Log the victim out
# - Buy / cancel an order
# - Post content
# If yes for any of them -> CSRF defence is missing on that route. Add it.
Why it matters
The CSRF defence chain is HTTP method discipline + SameSite=Lax cookies + a per-session token + a tight CORS allow-list. Each catches what the others miss. Drop any one and you have a single point of failure; keep all four and CSRF becomes a class of bug your tests prove cannot exist.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// CSRF tricks an authenticated browser into making an unwanted state change. // The browser auto-sends cookies; an attacker page can issue a POST to your origin // while the victim is logged in.Try it Yourself »
Exercise
Auth scheme that makes CSRF possible.
auth + state-changing endpoints = CSRF risk
Six letters.
Discussion
Loading…