CSRF (Cross-Site Request Forgery)
Forces an authenticated victim’s browser to send an unwanted request to an app where they have a session. The attacker never sees the response; they abuse the fact that the browser automatically attaches credentials (cookies) to any request aimed at that site, from whatever origin. If a state-changing action can be replayed from its structure alone and the app trusts only the cookie, it’s vulnerable. In essence, CSRF is an intent-verification failure: the app doesn’t check that the user meant to make the request.
sequenceDiagram
participant U as Victim (active session)
participant M as Attacker site
participant S as Target app
U->>M: visits the malicious page
M-->>U: auto-submitting form to S
U->>S: request carrying the session cookies
S-->>U: action executed as the victim
Threat model
Section titled “Threat model”Impact equals the action forced with the victim’s session:
- Change email or password → the attacker takes over the account (ATO) without knowing the current password.
- Transfers, purchases, plan changes.
- Create users or API keys, change permissions.
- Modify persistent rules: mail forwarding, webhooks, router DNS, account recovery.
- Login CSRF: force the victim to authenticate with the attacker’s account to capture their activity (searches, saved cards).
Anatomy
Section titled “Anatomy”Three necessary conditions:
- A relevant action worth triggering.
- Cookie-based session the browser sends on its own (not header tokens that JS must add).
- Predictable parameters: no secret the attacker can’t guess.
By design, the browser attaches the target’s cookies on any cross-site request. The
SameSite attribute modulates this:
None: always sent (requiresSecure).Lax(modern browser default): sent on top-level GET navigations (clicking a link), but not on cross-site POST or subresources.Strict: not sent on any cross-site request.
Also, JSON requests with Content-Type: application/json trigger a CORS
preflight, which blocks classic CSRF… unless the endpoint accepts “simple”
Content-Types (text/plain, application/x-www-form-urlencoded).
Variants
Section titled “Variants”- GET with effects: a tag that loads the URL is enough (
<img>,<link>). - POST: a self-submitting form.
- JSON/APIs: exploitable if the server accepts
text/plain/form or doesn’t validate the type (avoids preflight). - Multipart (
enctype=multipart/form-data): for endpoints expecting files. - Login CSRF: authenticate the victim with the attacker’s credentials.
- CSRF → XSS: when the forced action introduces a stored payload.
- CORS/clickjacking-assisted CSRF: read the token (lax CORS) or complete a flow (clickjacking) when a parameter is missing.
Red Team
Section titled “Red Team”Discovery
Section titled “Discovery”- Capture every state-changing action and check for an anti-CSRF token (hidden field, header, mirror cookie).
- If present, systematically try to:
- Remove it entirely.
- Leave it empty.
- Use a token from another session / account (is it session-bound?).
- Use a valid but expired token, or one with the correct structure.
- Switch
POST→GET(many frameworks accept both). - Change the
Content-Type(JSON →text/plain) to dodge preflight.
- Check the session cookie’s
SameSiteand whether the app validatesOrigin/Referer.
By hand: building the PoC
Section titled “By hand: building the PoC”<!-- 1) Self-submitting POST --><form action="https://bank.tld/email/change" method="POST" id="x"> <input type="hidden" name="email" value="attacker@evil.tld"></form><script>document.getElementById('x').submit()</script>
<!-- 2) GET with effects --><img src="https://bank.tld/transfer?to=attacker&amount=1000">
<!-- 3) JSON API without preflight (text/plain) --><form action="https://api.tld/v1/role" method="POST" enctype="text/plain"> <input name='{"role":"admin","x":"' value='"}'></form><script>document.forms[0].submit()</script>
<!-- 4) cross-site fetch (no-cors, for simple endpoints) --><script>fetch('https://bank.tld/email/change',{method:'POST',mode:'no-cors', credentials:'include',headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'email=attacker@evil.tld'});</script>Host the PoC on your domain and get the victim to open it (link, iframe, ad).
Advanced bypasses (explained)
Section titled “Advanced bypasses (explained)”- Default
SameSite=Lax: doesn’t cover top-level GET navigations. If a state-changing action acceptsGET, it’s still exploitable with a plain link. It also poorly covers the first two minutes after the cookie is issued (historic window in some browsers) and the jump between subdomains/sibling sites. - Method override: APIs accepting
_method=POSTorX-HTTP-Method-Overridelet you send asGET/form something meant to be POST. - Token not session-bound: if the token is valid across accounts, steal it from your own session and reuse it in the victim’s.
- Predictable / static token: if not random per session, it’s guessable.
- Weak double-submit: if validation compares cookie vs parameter and you can set the cookie (via a subdomain you control or an injection), you bypass the check.
- CORS misconfiguration: if the app reflects
OriginwithAllow-Credentials: true, read the token with an authenticatedfetchand send it yourself. - Bypassable
Referervalidation: when it only checks “is there a Referer”, omit it withReferrer-Policy: no-referrer(meta) or adata:context.
Tooling
Section titled “Tooling”Burp Suite (Engagement tools → Generate CSRF PoC), PoC-generation extensions, and
curl to validate token absence/omission.
Impact and chaining
Section titled “Impact and chaining”Direct account takeover (email/password change), privileged actions, persistence via rules (forwarding, webhooks), and CSRF→XSS→larger compromise. A login CSRF can capture sensitive data the victim enters thinking they’re in their own account.
Blue Team
Section titled “Blue Team”Detection
Section titled “Detection”- State-changing requests with absent or external
Origin/Referer. - Patterns of missing/invalid/reused token on sensitive endpoints.
- Anomalous cross-site referers to critical actions; coordinated spikes from one campaign (same malicious link).
- Email/password changes followed by activity from a new IP.
Telemetry and sources
Section titled “Telemetry and sources”Logs with Origin/Referer/Content-Type per request, WAF, and sensitive-action
metrics per user/origin.
Hardening
Section titled “Hardening”- Anti-CSRF token (synchronizer token): unpredictable value, session-bound, mandatory and server-validated on every state-changing action. Alternative: well-implemented double-submit (token in cookie + header, compared), robust only if the attacker can’t set the cookie.
SameSitecookies:Laxby default mitigates, but for critical actions useStrictand a token; don’t rely on the default alone.- Validate
Origin/Refererserver-side for requests with effects (reject if it doesn’t match your own origin). - APIs: require a custom header (
X-Requested-With) orAuthorization: Bearer(not auto-attached) and reject simpleContent-Types on JSON endpoints (forcingapplication/jsontriggers preflight). - Re-authentication / step-up for very sensitive actions (email change, transfers, account deletion).
Response
Section titled “Response”Invalidate affected sessions, revert forced changes (email/rules/passwords), notify
the victim, and add a token/Origin validation to the vulnerable endpoint.
CVEs and real-world cases
Section titled “CVEs and real-world cases”- ING Direct (2008) — Zeller & Felten’s academic paper demonstrated CSRF enabling transfers in online banking; a foundational case study.
- YouTube (2008) — CSRF allowing multiple actions on the victim’s account (favorites, subscriptions, messages).
- Home routers — waves of CSRF changing the router’s DNS from a malicious page (e.g. campaigns against routers in Brazil and the UK), redirecting all of the victim’s traffic.
- CMS plugins (WordPress, etc.) — CSRF (often chained with stored XSS) is one of the most frequent CVE types in plugins.
Product-specific CVEs in NVD (https://nvd.nist.gov/vuln/search) and GitHub Advisories (https://github.com/advisories).
Testing checklist
Section titled “Testing checklist”- All state-changing actions identified.
- Token presence and validation checked (remove/empty/other session/reuse).
-
POST→GETconversion andContent-Typechange (preflight) tested. -
SameSiteandOrigin/Referervalidation reviewed. - Method override and double-submit with a set cookie tested.
- CORS evaluated as a way to steal the token.
- Working PoC for at least one impactful action (ideally ATO).