Learn how to test for Cross-Site Request Forgery (CSRF) vulnerabilities, including missing token checks, weak SameSite cookies, method and Content-Type bypasses, and building a working proof-of-concept.
CSRF only matters for actions that change server state: updating an email or password, transferring funds, changing roles, deleting data, or adding an admin. Map every authenticated POST/PUT/PATCH/DELETE request and the parameters it sends. Capture a raw sample request to analyze — the HTTP Request Parser auto-extracts the method, cookies, tokens, and parameters for you.
POST /account/email HTTP/1.1
POST /transfer HTTP/1.1 (amount=100&to=attacker)
POST /admin/users/add HTTP/1.1
Look for a token in the request body, a custom header, or a hidden form field (commonly named csrf_token, _token, authenticity_token, or __RequestVerificationToken). Remove the token entirely and replay the request. If the action still succeeds, the endpoint has no CSRF protection at all.
csrf_token=<removed>
_token=<removed>
X-CSRF-Token: <header omitted>
A token that exists is not the same as a token that is validated. Submit an empty token, a token from another user's session, an old/reused token, or a token of the correct length but wrong value. If any of these are accepted, the validation is broken and the endpoint is exploitable.
csrf_token=
csrf_token=AAAAAAAAAAAAAAAAAAAAAAAA
csrf_token=<token-from-attacker-session>
csrf_token=<valid-but-already-used-token>
Modern browsers default session cookies to SameSite=Lax, which blocks cross-site POSTs but still allows top-level GET navigations. Inspect the Set-Cookie header for the SameSite attribute. SameSite=None (or absent on older targets) means cookies ride along on cross-site requests; Lax still permits GET-based CSRF and certain method downgrades.
Set-Cookie: session=...; SameSite=None; Secure
Set-Cookie: session=... (no SameSite attribute)
<!-- Lax still allows --> <a href="https://target/action?x=1">click</a>
Even when POST is protected, try downgrading the method to GET, since a GET request is trivially forgeable via an image or link. If the token is only checked for application/json, switch the Content-Type to one a form can send (text/plain, application/x-www-form-urlencoded, or multipart/form-data) to send a simple, cross-origin-allowed request. URL-encode parameter values as needed.
GET /account/[email protected]
Content-Type: text/plain (body: {"email":"[email protected]"})Content-Type: application/x-www-form-urlencoded
email%3Dattacker%40evil.com
Confirm the vulnerability with a self-submitting HTML form (or an <img> tag for GET) hosted on an attacker-controlled origin. Load it while authenticated to the target and verify the action executes without the user's intent. Document the affected endpoint, the missing or weak control, and the impact (account takeover, fund transfer, privilege escalation).
<form action="https://target/account/email" method="POST"><input name="email" value="[email protected]"></form><script>document.forms[0].submit()</script>
<img src="https://target/account/[email protected]">
<body onload="document.forms[0].submit()">
Level up your security testing
Install the CLI
npx payload-playgroundExplore All Tools
Encoding, hashing, JWT & more
Browse Cheat Sheets
Quick-reference payload guides