9 min read

Intermediate

CSRF Remediation

Risk Severity
High
Fix Effort
Low (Quick Fix)
Est. Time
1-2 hours
Reference
A01:2025 CWE-352

Cross-Site Request Forgery (CSRF) tricks authenticated users into executing unwanted actions. Proper token validation and cookie configurations provide robust protection.

CSRF Attack Flow

Understanding CSRF

Attack Scenario

  1. User logs into bank.com and receives session cookie
  2. User visits attacker's site while still logged in
  3. Attacker's page contains hidden form targeting bank.com
  4. Form auto-submits, browser includes session cookie
  5. Bank processes request as legitimate user action

CSRF Token Implementation

Best Practice

Use framework-provided CSRF protection when available. Most modern frameworks include robust CSRF middleware that handles token generation, validation, and rotation.

Token Generation

# Python/Flask - Generate secure CSRF token
import secrets

def generate_csrf_token():
    """Generate a cryptographically secure CSRF token"""
    token = secrets.token_hex(32)  # 64 character hex string
    session['csrf_token'] = token
    return token

# In template
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">

Token Validation

# Python/Flask - Validate CSRF token
from functools import wraps
from flask import request, session, abort

def csrf_protect(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if request.method in ['POST', 'PUT', 'DELETE', 'PATCH']:
            token = request.form.get('csrf_token') or request.headers.get('X-CSRF-Token')
            if not token or token != session.get('csrf_token'):
                abort(403, 'CSRF token invalid or missing')
        return f(*args, **kwargs)
    return decorated_function

@app.route('/transfer', methods=['POST'])
@csrf_protect
def transfer():
    # Process transfer
    pass

Express.js Implementation

// Node.js/Express with csurf middleware
const csrf = require('csurf');
const cookieParser = require('cookie-parser');

app.use(cookieParser());
app.use(csrf({ cookie: true }));

// Make token available to templates
app.use((req, res, next) => {
    res.locals.csrfToken = req.csrfToken();
    next();
});

// In template (EJS/Pug)
<input type="hidden" name="_csrf" value="<%= csrfToken %>">

// For AJAX requests
<meta name="csrf-token" content="<%= csrfToken %>">

// JavaScript
fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
        'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
});

SameSite Cookie Attribute

The SameSite attribute prevents the browser from sending cookies with cross-site requests, providing defense-in-depth against CSRF.

Strict

Cookie only sent in first-party context. Best CSRF protection but may break legitimate flows.

Lax (Recommended)

Cookie sent with top-level navigations and GET from external sites. Good balance.

None

Cookie sent in all contexts. Requires Secure flag. No CSRF protection.

# Python/Flask - Set SameSite cookie
response.set_cookie(
    'session',
    value=session_id,
    httponly=True,
    secure=True,  # Required for SameSite=None
    samesite='Lax'  # or 'Strict'
)

# Node.js/Express
res.cookie('session', sessionId, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax'
});

# PHP
setcookie('session', $sessionId, [
    'httponly' => true,
    'secure' => true,
    'samesite' => 'Lax'
]);

Framework-Specific Solutions

Django

# settings.py - CSRF is enabled by default
CSRF_COOKIE_SECURE = True  # Production only
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = 'Lax'

# In templates
{% csrf_token %}

# For AJAX (include in headers)
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
fetch('/api/', {
    headers: {'X-CSRFToken': csrftoken}
});

Spring Boot

// SecurityConfig.java - CSRF enabled by default
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .and()
            // ... other config
    }
}

// In Thymeleaf templates
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>

ASP.NET Core

// Startup.cs - Add antiforgery
services.AddAntiforgery(options => {
    options.HeaderName = "X-CSRF-TOKEN";
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Lax;
});

// In Razor views
@Html.AntiForgeryToken()

// In controller
[ValidateAntiForgeryToken]
[HttpPost]
public IActionResult Transfer(TransferModel model) { }

Testing Verification

CSRF Token Tests

# Test 1: Missing token should fail
curl -X POST https://site.com/transfer \
  -d "amount=100&to=attacker" \
  -b "session=valid_session_cookie"
# Expected: 403 Forbidden

# Test 2: Invalid token should fail  
curl -X POST https://site.com/transfer \
  -d "amount=100&to=attacker&csrf_token=invalid123" \
  -b "session=valid_session_cookie"
# Expected: 403 Forbidden

# Test 3: Token reuse should fail (if using one-time tokens)
# Use same token twice - second request should fail

SameSite Cookie Test

# Check SameSite attribute on session cookie
curl -I https://yoursite.com/login -c - | grep -i "set-cookie"

# Expected: SameSite=Lax or SameSite=Strict in cookie header

Common Mistakes

❌ Token in GET Parameters

<!-- DON'T: Token leaked in URLs/logs -->
<a href="/delete?csrf=abc123">Delete</a>

Tokens in URLs get logged and leaked via Referer

✅ Correct Approach

<!-- DO: Token in hidden form field -->
<form method="POST">
  <input type="hidden" name="csrf" value="...">
</form>

POST body is not logged or in Referer

❌ Weak Token Validation

# DON'T: Only check if token exists
if request.form.get('csrf_token'):
    process_request()

Any token value would be accepted

✅ Correct Approach

# DO: Compare tokens securely
import hmac
if hmac.compare_digest(
    request.form['csrf'], session['csrf']):
    process_request()

Timing-safe comparison prevents attacks

Verification Checklist

  • ☐ All state-changing requests require valid CSRF token
  • ☐ Tokens are cryptographically random (min 128 bits)
  • ☐ Tokens are validated server-side on every request
  • ☐ SameSite cookie attribute set to Lax or Strict
  • ☐ Tokens are rotated after login
  • ☐ AJAX requests include CSRF header
  • ☐ Framework CSRF middleware is enabled
  • ☐ No state changes via GET requests

CSRF Security Labs

Practice identifying and exploiting CSRF vulnerabilities.

CSRF Vulnerabilities PortSwigger easy
Token bypassSameSite bypassReferer validation bypass

Evidence to Capture

The cross-origin page: The full HTML you served from your own origin — self-submitting form or fetch — and the request it produced at the target, including the victim cookie and the Origin and Referer headers.

Resulting state change: The victim resource read back before and after the forged request: the changed email address, the new API key, the transfer row with its id and timestamp. A 200 alone is not a state change.

Token behaviour: The same endpoint called with no token, with a stale token, with a token from another session, and with the token duplicated in body and header, each with its status and body.

Cookie attributes: The exact Set-Cookie line for the authenticating cookie, showing SameSite and Secure, because SameSite decides whether the forged request carries credentials at all.

Method and content type: Which methods and content types the endpoint accepts. A form-encoded POST is cross-site reachable without a preflight, and that combination is what makes the finding real.

Likely False Positives

  • State change on a safe method: A GET that alters data is a method-design defect, not CSRF, because the attacker cannot set a custom header and browsers may cache it. Report it separately.
  • Token enforced, action still executed by the owner: If your replay used the victim session cookie from your own browser after a fresh login, the token was present. Remove it and confirm the 403 before claiming anything.
  • SameSite=Lax blocking the forgery: A cross-site POST without the cookie succeeds only as an anonymous request. If the action requires authentication, there is no CSRF, and the finding is limited to login CSRF.
  • Bearer-only API: An API that accepts only an Authorization header and no cookie is not CSRF exposed. Cross-origin JavaScript cannot set that header without a CORS grant.
  • Preflight failure read as protection: A blocked CORS preflight protects JSON with a custom header, not form-encoded or text-plain submissions. Test the simple request before concluding the endpoint is protected.

Testing Checklist

Discovery

  • ☐ List every state-changing endpoint with its accepted methods and content types, and note which authenticate by cookie.
  • ☐ Record SameSite, Secure and scope for each session cookie, and whether the app also accepts the session in a query parameter.
  • ☐ Find the flows that broaden the attack: password change, email change, MFA enrolment, API key creation, fund transfer.

Confirmation

  • ☐ Replay the action from a different origin with the victim session cookie and no token, then with an empty and a stale token.
  • ☐ Try the validation bypasses: token from another session, duplicate parameters, method override headers, and a JSON body on an endpoint that only checks form fields.
  • ☐ Confirm the resulting state from a separate authenticated read, so the finding is the change rather than the response code.

Impact

  • ☐ Choose an action whose forgery matters to the victim — credential or MFA change, key creation, payment — and show it in one click from the attacker page.
  • ☐ State whether SameSite alone would have stopped it, because that determines whether the fix is a token or a cookie attribute.
  • ☐ Check whether the forged action is repeatable and whether it survives the victim logging out.

Remediation

Structural fix

  • • Require a per-session synchroniser token on every unsafe method, validated server-side with a constant-time compare, and issue it by the framework mechanism rather than by hand.
  • • Set SameSite=Lax or Strict on the session cookie and stop changing state on GET.
  • • Rotate the token at login and privilege change, and scope it to the session so a token from another session cannot validate.

Compensating control

  • • For JSON APIs that cannot carry a form token, validate Origin or Referer against an allowlist as defence in depth, and require a custom header that a simple cross-site request cannot set.
  • • Require re-authentication for the highest-impact actions, so a forged request alone cannot complete them.

CWE References: CWE-352