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

  1. Victim logs into a legitimate site (e.g. mymechanic.com) - gets a session cookie
  2. Browser stores that cookie and automatically sends it with every request to that domain
  3. Victim clicks a malicious link (phishing, XSS redirect, anything)
  4. The malicious page fires a hidden POST request to mymechanic.com
  5. Browser attaches the session cookie automatically
  6. The server sees a valid cookie - thinks it is a legitimate request from the victim
  7. 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-Cookie header when logging in - is there a SameSite flag?
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" with type="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

  1. Log in as the victim (Jeremy) in Container 1 - note the current state (email = jeremy@jeremy.com)
  2. Open the PoC in Container 2 or a new tab (cookie is still active)
  3. Submit the form
  4. 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:

TestHow
Remove token entirelyDelete the csrf field from the request
Send random valueChange value to 1234 or alexwashere
Send empty valuecsrf=
Send very long value500 random characters
Send token from different userLog in as Jessamy, copy her token, use in Jeremy’s request
Send old/expired tokenGrab a token, wait, reuse it
Change POST to GETMove the parameters to the URL: csrf0x02.php?email=x&csrf=y
Test predictabilityIs 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:

  1. Send a request to the page to fetch the current CSRF token
  2. Extract the token from the response
  3. Inject it into your forged POST request
  4. 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 to text/plain still 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

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 the SameSite cookie flag
  • Check the Set-Cookie header: no SameSite flag + 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)