Cross-Site Request Forgery (CSRF)
Trick users into performing an action within a webapp they are logged into - without them knowing. Examples: changing email/password, transferring funds, posting on forums, modifying account settings.
How it works
- Victim logs into a legitimate site (e.g. mymechanic.com) - gets a session cookie
- Browser stores that cookie and automatically sends it with every request to that domain
- Victim clicks a malicious link (phishing, XSS redirect, anything)
- The malicious page fires a hidden POST request to mymechanic.com
- Browser attaches the session cookie automatically
- The server sees a valid cookie - thinks it is a legitimate request from the victim
- Action is performed as the victim - they never knew
The attacker never touches the cookie. The victim’s browser is the weapon.
Attack Methodology - Step by Step
Step 1 - Find a sensitive state-changing action
Not every request matters. Focus on POST requests that do something meaningful:
- Change email / password / username
- Transfer money / change payment info
- Add/remove users
- Change account settings
- Post content as the user
GET requests almost never qualify (they should not change state).
Step 2 - Inspect the request for CSRF protection
In Burp HTTP History or browser DevTools, look at the POST request body and headers. Ask:
- Is there a hidden field with a random token? (
csrf,_token,authenticity_token,nonce,__RequestVerificationToken) - Is there a custom header? (
X-Requested-With,X-CSRF-Token) - Check the
Set-Cookieheader when logging in - is there aSameSiteflag?
Set-Cookie: session=abc123; SameSite=Strict <- blocks CSRF, cookie not sent cross-site
Set-Cookie: session=abc123; SameSite=Lax <- blocks most CSRF (not sent on cross-site POSTs)
Set-Cookie: session=abc123; <- no protection, likely vulnerable
No token + no SameSite = almost certainly vulnerable, move straight to PoC. Token present = do not give up, test the token (see below).
Step 3 - Build the PoC
Take the POST request from Burp. Every key=value pair in the body becomes a hidden input in the form.
Example POST body: email=hacker@hacker.com
<!DOCTYPE html>
<html>
<body>
<form method="POST" action="http://localhost:80/labs/csrf0x01.php">
<input type="hidden" name="email" value="hacker@hacker.com">
</form>
<script>window.onload = function(){ document.forms[0].submit(); }</script>
</body>
</html>Using the free Burp plugin (https://i0akinsec.wordpress.com/2016/06/06/csrf-poc-creator-for-burp-suite-free-edition/):
- Intercept the correct POST request (the email update, not the login)
- Right-click in HTTP history → send to the extension → copy HTML
- The plugin generates the form - just make sure it captured the right request
- Replace
type="text"withtype="hidden"and add the auto-submit script
If you are testing yourself (playing both attacker and victim), the submit button is fine. Auto-submit only matters when delivering to a real victim.
Step 4 - Confirm the vulnerability
- Log in as the victim (Jeremy) in Container 1 - note the current state (email =
jeremy@jeremy.com) - Open the PoC in Container 2 or a new tab (cookie is still active)
- Submit the form
- Check if state changed (email is now
hacker@hacker.com)
State changed = confirmed vulnerable. Screenshot before and after for the report.
Token Bypass - Lab 2
Just because there is a CSRF token does not mean it is implemented correctly. The token existed in lab 2 but the server only checked that the field was present - not that the value matched.
Example POST with token:
email=jeremy@jeremy.com&csrf=RANDOMSTRING
PoC with token field (any value):
<!DOCTYPE html>
<html>
<body>
<form method="POST" action="http://localhost:80/labs/csrf0x02.php">
<input type="hidden" name="email" value="hacked@hacked.com">
<input type="hidden" name="csrf" value="alexwashere">
</form>
<script>window.onload = function(){ document.forms[0].submit(); }</script>
</body>
</html>If this works - the server checks existence, not value. Vulnerable.
Full token bypass test checklist
Work through these one by one. Any “yes” = broken implementation:
| Test | How |
|---|---|
| Remove token entirely | Delete the csrf field from the request |
| Send random value | Change value to 1234 or alexwashere |
| Send empty value | csrf= |
| Send very long value | 500 random characters |
| Send token from different user | Log in as Jessamy, copy her token, use in Jeremy’s request |
| Send old/expired token | Grab a token, wait, reuse it |
| Change POST to GET | Move the parameters to the URL: csrf0x02.php?email=x&csrf=y |
| Test predictability | Is the token always the same? Short? Numeric only? |
CSRF token + XSS = bypass
If the app has both CSRF protection and an XSS vulnerability - the XSS wins. You can use XSS to:
- Send a request to the page to fetch the current CSRF token
- Extract the token from the response
- Inject it into your forged POST request
- Send the request
CSRF tokens are only as strong as the XSS protection on the same site.
What to Look for on the Exam (Harder Scenarios)
The lab was straightforward. The exam will not be. Things to try when it is not obvious:
- Different HTTP methods - try GET instead of POST, try PUT/PATCH/DELETE
- JSON body - some apps accept
Content-Type: application/json. Harder to CSRF but not impossible (check if changing content-type totext/plainstill works) - Referer/Origin header checks - server may check where the request came from. Try removing these headers entirely, or sending an empty Referer
- Token in URL not body - sometimes the token is a URL parameter, not a form field
- Double submit cookie pattern - token is both in a cookie and in the body. Check if cookie and body value just need to match each other (attacker can set both)
- Multi-step flows - the vulnerable action might be step 3 of a 3-step process. You may need to chain multiple requests
- Logout CSRF - force a victim to log out
- Login CSRF - log a victim into your account to track their activity
Resources
- AppSec Explained checklist: https://appsecexplained.gitbook.io/appsecexplained/common-vulns/cross-site-request-forgery-csrf
- OWASP CSRF Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
Testing Checklist
- Find sensitive state-changing POST requests: email/password changes, fund transfers, account settings, user management - GET requests almost never qualify
- Inspect the request for CSRF protection: look for hidden token fields (
csrf,_token,nonce), custom headers (X-CSRF-Token), and theSameSitecookie flag - Check the
Set-Cookieheader: noSameSiteflag + no token = almost certainly vulnerable. Move straight to PoC - If a CSRF token is present, test if the server actually validates the value: send a random value (
csrf=1234), an empty value (csrf=), or remove the field entirely - Test cross-user token validity: grab a token from a different user session and use it in the target’s request
- Test HTTP method switching: change POST to GET and move parameters to the URL - some apps accept both
- Build a PoC HTML form: convert every POST body key=value pair into a hidden input, set the form action to the target URL, add auto-submit JavaScript (
window.onload = function(){ document.forms[0].submit(); }) - Confirm the vulnerability using browser containers: log in as the victim in Container 1, open the PoC in Container 2, verify state changed (e.g., email updated)
- Take before/after screenshots showing the state change for the report
- If the app has both CSRF protection and an XSS vulnerability, consider chaining: use XSS to fetch the CSRF token from the page, extract it, and inject it into a forged request
- For harder scenarios, test: JSON body requests (try switching Content-Type to
text/plain), Referer/Origin header removal, double-submit cookie patterns, and multi-step flows - Include in report: the vulnerable endpoint, what protection was missing or broken, the PoC HTML, before/after screenshots, and the impact (e.g., attacker can change victim’s email and take over account)