Cross-Site Request Forgery (CSRF)
Victim’s authenticated browser is tricked into firing a state-changing request. Relies purely on cookies/session being auto-attached by the browser, attacker never sees the response (blind).
Types
Traditional (form-based)
Auto-submitting HTML form on attacker page, POSTs to the target while victim’s cookies ride along.
<form action="https://target/transfer" method="POST" id="f">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="1000">
</form>
<script>document.getElementById('f').submit();</script>GET-based / hidden link-image
No form needed if the vulnerable action accepts GET. A bare
<img src> or <a href> is
enough, no JS, no click confirmation dialog like a redirect.
<img src="https://target/transfer?to=attacker&amount=1000" width="0" height="0">Look for state-changing actions still wired to GET, these are the easiest wins and don’t even need a landing page, an email with an image tag works.
XHR/Fetch (async/JSON APIs)
Same trust exploited, just via
fetch/XMLHttpRequest instead of a form.
Matters because JSON body APIs are often assumed “safe” from CSRF
since forms can’t send application/json without
triggering a CORS preflight. Bypass: send as text/plain
or standard form-urlencoded, most frameworks parse the body
regardless of declared content-type.
<script>
fetch('https://target/api/updateEmail', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'text/plain'},
body: JSON.stringify({email: '[email protected]'})
});
</script>Also check if the endpoint accepts a form-encoded equivalent of the JSON fields, kills the “it’s JSON so it’s CSRF-safe” assumption instantly.
CORS misconfig CSRF
Access-Control-Allow-Origin: * combined with
credentials read back (not just fire-and-forget) lets attacker read
the response too, not just trigger the action. Check for reflected
Origin header (ACAO: <attacker origin> mirrored
back) plus Access-Control-Allow-Credentials: true,
that’s a full read/write CSRF, worse than blind.
Flash-based (legacy)
Malicious .swf on attacker’s site abusing
crossdomain.xml misconfig
(allow-access-from domain="*"). Dead tech since Flash
EOL Dec 2020, only relevant on legacy/embedded systems still serving
Flash content.
Token implementation weaknesses to check
Presence of a CSRF token doesn’t mean it’s actually protecting anything. Check:
- Not tied to session: token accepted regardless of which session it was issued to, grab any valid token (even your own account’s) and replay it against the victim’s session.
- Predictable/reversible: token is just
base64(account_id)or similarly derived, not random. Base64/hex decode every token you see before assuming it’s opaque, CyberChef magic wand. - Double-submit cookie without server-side binding: server only checks cookie value == form value, doesn’t verify the cookie was actually set by the legit server for that session. If attacker can set a cookie for the domain (subdomain XSS, subdomain takeover, related-domain trust), they can set both halves themselves.
- Not invalidated after use: same token works across multiple requests/sessions, no rotation.
- Missing on some endpoints: token enforced on the main flow but a legacy/alternate endpoint (mobile API, password reset, account deletion) skips validation entirely.
- Method mismatch: token checked on POST but the same action also works via GET.
- Static per-user token leaking: token embedded in page HTML/JS bundle, source-visible, or leaked via Referer to third-party resources on the same page.
Subdomain cookie injection angle
If attacker controls or can inject into
attacker.target.com (XSS on a subdomain, forgotten dev
subdomain, subdomain takeover), they can set a cookie scoped to
domain=target.com that overrides the double-submit
cookie for the parent domain, then auto-submit a form with the
matching token value. Chain: subdomain compromise +
predictable/known token value + double-submit pattern.
Bypass checklist when pentesting
- Try removing the token param entirely, does it still succeed
- Try an empty/malformed token value
- Try swapping the token for one from a different session/account
- Change request method (POST to GET, or vice versa) on the same endpoint
- Change
Content-Type(json to text/plain, or to multipart) to dodge preflight while keeping the same server-side parsing - Strip/spoof
RefererandOriginheaders, see if either is actually enforced (browser extensions, meta-referrer, orReferer-Policy: no-referreron the attacker page can drop it naturally) - Check
SameSitecookie attribute,Laxstill allows top-level GET navigations to carry cookies, so GET-based state changes (login CSRF included) surviveLax. OnlyStrictblocks that. - Look for a “convert to GET” trick via
X-HTTP-Method-Overrideheader or_methodparam, some frameworks honor it and skip CSRF middleware that’s only wired for POST - If token is in a custom header instead of body/cookie, simple
form/img CSRF can’t set custom headers, but check if a
<form>with the header value smuggled into a URL, or a misconfigured CORS endpoint, gets around it - Decode every token, more often than not it is an ID, timestamp, or username in disguise
Defence quick reference
- Per-session, per-request random token, validated server-side against the session, not just cookie==body
SameSite=StrictorLaxon session cookies,Strictif login CSRF is in scope- Verify
Origin/Refereras defense in depth, not the only control - Don’t allow state-changing actions on GET
- Tight CORS: explicit origin allowlist, never
*with credentials - Re-authentication (password/OTP) for sensitive actions (email change, password change, fund transfer)