---
# === IDENTITY ===
id: software/security/csrf-prevention/2026
canonical_question: "How do I prevent Cross-Site Request Forgery (CSRF)?"
aliases:
  - "CSRF protection best practices"
  - "cross-site request forgery mitigation"
  - "anti-CSRF token implementation"
  - "SameSite cookie CSRF defense"
  - "synchronizer token pattern"
  - "double submit cookie CSRF"
  - "CWE-352 prevention"
  - "how to stop CSRF in Django Express Spring"
entity_type: software_reference
domain: software > security > CSRF Prevention
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-02-27
confidence: 0.95
version: 1.0
first_published: 2026-02-27

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "SameSite=Lax became browser default (Chrome 80, Feb 2020); csurf npm package deprecated (Sep 2022); Fetch Metadata Sec-Fetch-* headers widely supported (Mar 2023)"
  next_review: 2026-08-26
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "NEVER use GET requests for state-changing operations -- CSRF tokens cannot protect GET endpoints"
  - "XSS defeats ALL CSRF defenses -- fix XSS first before relying on CSRF tokens"
  - "SameSite cookies are defense-in-depth, NOT a standalone CSRF solution"
  - "CSRF tokens MUST be compared using constant-time comparison to prevent timing attacks"
  - "NEVER transmit CSRF tokens in URL query strings -- they leak via Referer headers and server logs"
  - "The deprecated csurf npm package has known bypasses -- use csrf-csrf or framework-native protection instead"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need to prevent Cross-Site Scripting (XSS), not CSRF"
    use_instead: "software/security/xss-prevention/2026"
  - condition: "Need SQL injection prevention"
    use_instead: "software/security/sql-injection-prevention/2026"
  - condition: "Building a stateless API with JWT (no session cookies)"
    use_instead: "software/patterns/jwt-implementation/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "framework"
    question: "What web framework or language are you using?"
    type: choice
    options: ["Django", "Express/Node.js", "Spring Boot", "Rails", "Laravel", "ASP.NET", "Go", "Other"]
  - key: "request_type"
    question: "Are state-changing requests sent via HTML forms, AJAX/fetch, or both?"
    type: choice
    options: ["HTML forms only", "AJAX/fetch only", "Both forms and AJAX"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/security/csrf-prevention/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-27)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/security/xss-prevention/2026"
      label: "XSS Prevention"
    - id: "software/patterns/session-management/2026"
      label: "Session Management Patterns"
  solves: []
  alternative_to: []
  often_confused_with:
    - id: "software/security/xss-prevention/2026"
      label: "XSS Prevention (scripting, not request forgery)"
    - id: "software/security/sql-injection-prevention/2026"
      label: "SQL Injection Prevention"

# === SOURCES (7 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "Cross-Site Request Forgery Prevention Cheat Sheet"
    author: OWASP Foundation
    url: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
    type: community_resource
    published: 2025-12-24
    reliability: authoritative
  - id: src2
    title: "How to prevent CSRF vulnerabilities"
    author: PortSwigger
    url: https://portswigger.net/web-security/csrf/preventing
    type: technical_blog
    published: 2025-01-15
    reliability: high
  - id: src3
    title: "Cross-site request forgery (CSRF)"
    author: MDN Web Docs
    url: https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/CSRF
    type: official_docs
    published: 2025-06-01
    reliability: authoritative
  - id: src4
    title: "CWE-352: Cross-Site Request Forgery (CSRF)"
    author: MITRE
    url: https://cwe.mitre.org/data/definitions/352.html
    type: official_docs
    published: 2024-12-01
    reliability: authoritative
  - id: src5
    title: "Cross Site Request Forgery (CSRF) :: Spring Security"
    author: Spring
    url: https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html
    type: official_docs
    published: 2025-03-01
    reliability: high
  - id: src6
    title: "Cross Site Request Forgery protection"
    author: Django Software Foundation
    url: https://docs.djangoproject.com/en/6.0/ref/csrf/
    type: official_docs
    published: 2025-12-01
    reliability: high
  - id: src7
    title: "Protect your resources from web attacks with Fetch Metadata"
    author: Google Web Dev
    url: https://web.dev/articles/fetch-metadata
    type: technical_blog
    published: 2024-03-01
    reliability: high
---

# CSRF Prevention: Cross-Site Request Forgery Defense Guide

## TL;DR

- **Bottom line**: Combine CSRF tokens (synchronizer or signed double-submit cookie pattern) with SameSite cookie attributes and Fetch Metadata header validation for layered CSRF defense -- no single technique is sufficient alone.
- **Key tool/command**: `{% csrf_token %}` (Django), `csrf-csrf` middleware (Express), `http.csrf(Customizer.withDefaults())` (Spring Security) -- use your framework's built-in CSRF protection.
- **Watch out for**: XSS vulnerabilities defeat ALL CSRF protections -- an attacker who can inject scripts can read and submit tokens.
- **Works with**: All major web frameworks have built-in CSRF support. SameSite cookies supported in all modern browsers (Chrome 51+, Firefox 60+, Safari 12+, Edge 16+). Fetch Metadata supported since March 2023.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- NEVER use GET requests for state-changing operations -- CSRF tokens cannot protect GET endpoints
- XSS defeats ALL CSRF defenses -- fix XSS first before relying on CSRF tokens
- SameSite cookies are defense-in-depth, NOT a standalone CSRF solution
- CSRF tokens MUST be compared using constant-time comparison to prevent timing attacks
- NEVER transmit CSRF tokens in URL query strings -- they leak via Referer headers and server logs
- The deprecated csurf npm package has known bypasses -- use csrf-csrf or framework-native protection instead

## Quick Reference

| # | Attack Vector | Risk | Vulnerable Pattern | Secure Pattern |
|---|---|---|---|---|
| 1 | Hidden form POST | Critical | Form action targets victim site, browser auto-sends session cookie | Require CSRF token in hidden field, validate server-side |
| 2 | Auto-submitting form via JS | Critical | `<form>` + `document.forms[0].submit()` on attacker page | SameSite=Strict cookie + CSRF token validation |
| 3 | Image tag GET | High | `<img src="https://bank.com/transfer?to=attacker">` | Never use GET for state changes; validate Sec-Fetch-Site |
| 4 | XHR/fetch from attacker origin | High | No CORS preflight for simple requests (`application/x-www-form-urlencoded`) | Require custom header (`X-CSRF-Token`) or `application/json` Content-Type |
| 5 | Subdomain cookie tossing | High | Naive double-submit cookie pattern without HMAC signing | Use signed double-submit cookie with HMAC and session binding |
| 6 | Login CSRF | Medium | No CSRF protection on login form | Use pre-session token for unauthenticated forms |
| 7 | Cross-origin WebSocket | Medium | WebSocket handshake carries cookies without origin check | Validate Origin header on WebSocket upgrade |
| 8 | Flash/Silverlight-based (legacy) | Low | Outdated plugins bypass SameSite restrictions | Deprecated; ensure plugins are disabled |

**Prevention Methods Comparison:**

| # | Method | Strength | Stateful? | Framework Support | Limitations |
|---|---|---|---|---|---|
| 1 | Synchronizer Token Pattern | Strong | Yes (server session) | Django, Rails, Spring, ASP.NET | Requires server-side session storage |
| 2 | Signed Double-Submit Cookie | Strong | No (stateless) | csrf-csrf (Express), custom | Must HMAC-sign with server secret; naive version is vulnerable |
| 3 | Fetch Metadata (Sec-Fetch-Site) | Strong | No | Express middleware, Django ticket pending | Legacy browsers without Fetch Metadata need fallback |
| 4 | Custom Request Headers | Moderate | No | Angular (X-XSRF-TOKEN), Axios | Only works for AJAX; forms cannot set custom headers |
| 5 | SameSite Cookies | Moderate | No | All browsers (default Lax) | Bypassed by same-site cross-origin attacks; GET still sends Lax cookies |
| 6 | Origin/Referer Validation | Moderate | No | Manual implementation | Referer can be stripped; Origin absent in some requests |

## Decision Tree

```
START: What type of state-changing requests does your app make?
├── HTML form submissions (POST)?
│   ├── YES → Use Synchronizer Token Pattern (hidden field + server session)
│   │         Django: {% csrf_token %}, Spring: _csrf.token, Rails: authenticity_token
│   └── NO ↓
├── AJAX/fetch only (SPA architecture)?
│   ├── YES → Require custom header (X-CSRF-Token) or use application/json Content-Type
│   │         + signed double-submit cookie for stateless validation
│   └── NO ↓
├── Both forms and AJAX?
│   ├── YES → Synchronizer token for forms + custom header with same token for AJAX
│   └── NO ↓
├── Stateless API with JWT (no session cookies)?
│   ├── YES → CSRF protection not needed (no cookies = no CSRF risk)
│   └── NO ↓
├── Microservices behind API gateway?
│   ├── YES → Validate Fetch Metadata (Sec-Fetch-Site) at gateway level
│   └── NO ↓
└── DEFAULT → Synchronizer Token Pattern + SameSite=Lax cookies + Fetch Metadata validation
```

## Step-by-Step Guide

### 1. Set SameSite attribute on all session cookies

SameSite prevents the browser from sending cookies with cross-site requests. Set it to `Strict` where possible, `Lax` as minimum. [src1]

```http
Set-Cookie: sessionid=abc123; SameSite=Lax; Secure; HttpOnly; Path=/
```

For maximum protection with cookie prefixes:

```http
Set-Cookie: __Host-sessionid=abc123; SameSite=Strict; Secure; HttpOnly; Path=/
```

**Verify**: Use browser DevTools > Application > Cookies -- confirm SameSite attribute is set. Test cross-site form submission -- cookie should NOT be sent with POST requests.

### 2. Implement framework-native CSRF token protection

Use your framework's built-in CSRF middleware rather than building your own. [src6]

**Django** -- built-in middleware:
```python
# settings.py -- enabled by default
MIDDLEWARE = [
    'django.middleware.csrf.CsrfViewMiddleware',
    # ...
]

# In templates:
# <form method="post">{% csrf_token %} ... </form>
```

**Express/Node.js** -- csrf-csrf package:
```javascript
const { doubleCsrf } = require('csrf-csrf');  // ^3.0.0
const { doubleCsrfProtection, generateToken } = doubleCsrf({
  getSecret: () => process.env.CSRF_SECRET,
  cookieName: '__Host-csrf',
  cookieOptions: { sameSite: 'strict', secure: true, httpOnly: true }
});
app.use(doubleCsrfProtection);
```

**Spring Boot** -- enabled by default:
```java
// Spring Security 6.x -- CSRF enabled by default
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.csrf(Customizer.withDefaults());
    return http.build();
}
```

**Verify**: Submit a form without the CSRF token -- should receive 403 Forbidden.

### 3. Add Fetch Metadata validation middleware

Fetch Metadata headers let you reject cross-site requests at the server level before any application logic runs. [src7]

```javascript
// Express.js Fetch Metadata middleware
function fetchMetadataPolicy(req, res, next) {
  const site = req.headers['sec-fetch-site'];

  // Allow requests from browsers that don't send Fetch Metadata
  if (!site) return next();

  // Allow same-origin, same-site, and direct navigations
  if (['same-origin', 'same-site', 'none'].includes(site)) return next();

  // Allow safe top-level navigations (GET only)
  if (req.headers['sec-fetch-mode'] === 'navigate'
      && req.method === 'GET'
      && !['object', 'embed'].includes(req.headers['sec-fetch-dest'])) {
    return next();
  }

  // Block all other cross-site requests
  res.status(403).json({ error: 'Cross-site request blocked' });
}

app.use(fetchMetadataPolicy);
```

**Verify**: Send a cross-origin POST request from a different domain -- should receive 403. Same-origin requests should pass.

### 4. Validate Origin and Referer headers as additional defense

Check that the Origin (or Referer) header matches your expected domain on state-changing requests. [src1]

```python
# Django -- CsrfViewMiddleware already validates Origin header
# For custom implementations:
ALLOWED_ORIGINS = {'https://example.com', 'https://www.example.com'}

def validate_origin(request):
    origin = request.headers.get('Origin')
    if origin and origin not in ALLOWED_ORIGINS:
        return False
    # Fallback to Referer if Origin is absent
    referer = request.headers.get('Referer')
    if referer:
        from urllib.parse import urlparse
        parsed = urlparse(referer)
        return f"{parsed.scheme}://{parsed.netloc}" in ALLOWED_ORIGINS
    return True  # Allow if neither header present (direct navigation)
```

**Verify**: Set Origin header to `https://evil.com` in a request tool -- should be rejected.

### 5. Protect login and unauthenticated forms

Login forms are vulnerable to CSRF (login CSRF) even without an active session. Use a pre-session token. [src1]

```python
# Django -- csrf_protect decorator works on login views too
from django.views.decorators.csrf import csrf_protect

@csrf_protect
def login_view(request):
    if request.method == 'POST':
        # Django's CsrfViewMiddleware validates the token
        # even before the user has a session
        pass
```

**Verify**: Submit the login form without `csrfmiddlewaretoken` -- should return 403.

## Code Examples

### Python/Django: Complete CSRF Protection

> Full script: [python-django-complete-csrf-protection.py](scripts/python-django-complete-csrf-protection.py) (34 lines)

```python
# settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',  # CSRF enabled
    # ...
# ... (see full script)
```

### Node.js/Express: Signed Double-Submit Cookie

> Full script: [node-js-express-signed-double-submit-cookie.js](scripts/node-js-express-signed-double-submit-cookie.js) (36 lines)

```javascript
const express = require('express');            // ^4.18.0
const { doubleCsrf } = require('csrf-csrf');   // ^3.0.0
const cookieParser = require('cookie-parser'); // ^1.4.0
const app = express();
app.use(cookieParser());
# ... (see full script)
```

### Java/Spring Boot: Built-in CSRF with Thymeleaf

> Full script: [java-spring-boot-built-in-csrf-with-thymeleaf.java](scripts/java-spring-boot-built-in-csrf-with-thymeleaf.java) (28 lines)

```java
// SecurityConfig.java -- Spring Security 6.x
import org.springframework.security.config.Customizer;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
# ... (see full script)
```

## Anti-Patterns

### Wrong: Using GET for state-changing operations

```html
<!-- BAD -- GET requests are not protected by CSRF tokens -->
<a href="/api/delete-account?confirm=true">Delete Account</a>
<!-- An attacker can embed: <img src="/api/delete-account?confirm=true"> -->
```

### Correct: Use POST with CSRF token for state changes

```html
<!-- GOOD -- POST with CSRF token -->
<form method="POST" action="/api/delete-account">
  <input type="hidden" name="csrf_token" value="{{csrf_token}}">
  <button type="submit">Delete Account</button>
</form>
```

### Wrong: Naive double-submit cookie without HMAC signing

```javascript
// BAD -- unsigned double-submit cookie is vulnerable to cookie tossing
// An attacker controlling a subdomain can set their own csrf cookie
const token = crypto.randomBytes(32).toString('hex');
res.cookie('csrf', token);
// Then check req.cookies.csrf === req.body._csrf
// Attacker sets csrf cookie via subdomain + submits matching value
```

### Correct: Signed double-submit cookie with HMAC

```javascript
// GOOD -- HMAC-signed token bound to session
const crypto = require('crypto');
function generateCsrfToken(sessionId, secret) {
  const random = crypto.randomBytes(16).toString('hex');
  const hmac = crypto.createHmac('sha256', secret)
    .update(sessionId + random).digest('hex');
  return hmac + '.' + random;
}
function validateCsrfToken(token, sessionId, secret) {
  const [hmac, random] = token.split('.');
  const expected = crypto.createHmac('sha256', secret)
    .update(sessionId + random).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(expected));
}
```

### Wrong: Disabling CSRF protection for convenience

```java
// BAD -- disabling CSRF on session-based app
http.csrf(csrf -> csrf.disable());
// "It works now!" -- and so do CSRF attacks
```

### Correct: Disable only for stateless JWT APIs

```java
// GOOD -- disable CSRF only when session cookies are not used
http
  .csrf(csrf -> csrf.disable())
  .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
  .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
// No session cookie = no CSRF risk
```

### Wrong: Using the deprecated csurf package

```javascript
// BAD -- csurf is deprecated (Sep 2022) with known bypasses
const csurf = require('csurf');  // DO NOT USE
app.use(csurf());
// Vulnerable to cookie tossing via subdomain control
```

### Correct: Use csrf-csrf or framework-native protection

```javascript
// GOOD -- csrf-csrf implements signed double-submit cookie pattern
const { doubleCsrf } = require('csrf-csrf');  // ^3.0.0
const { doubleCsrfProtection } = doubleCsrf({
  getSecret: () => process.env.CSRF_SECRET,
  cookieName: '__Host-csrf',
  cookieOptions: { sameSite: 'strict', secure: true, httpOnly: true }
});
app.use(doubleCsrfProtection);
```

## Common Pitfalls

- **SameSite=Lax allows GET requests**: Lax only blocks cross-site POST/PUT/DELETE. If your app changes state via GET (e.g., `/api/delete?id=5`), SameSite=Lax provides no protection. Fix: Never use GET for state changes. [src1]
- **CSRF token in cookie only**: Placing the token only in a cookie (not also in a form field or header) provides zero protection -- browsers auto-send cookies on cross-site requests. Fix: Token must appear in both a cookie AND a request body/header. [src2]
- **Missing CSRF on login forms**: Login CSRF allows an attacker to log the victim into the attacker's account. Fix: Apply CSRF protection to login and other pre-authentication forms. [src1]
- **XSS negates CSRF protection**: An XSS vulnerability allows the attacker to read CSRF tokens from the DOM and submit forged requests. Fix: Prevent XSS first; CSRF defense is only effective when the attacker cannot inject scripts. [src4]
- **Cross-origin same-site attacks**: SameSite cookies protect against cross-site but NOT cross-origin same-site requests. A subdomain `evil.blog.example.com` can still send requests to `bank.example.com`. Fix: Use CSRF tokens in addition to SameSite; use `__Host-` cookie prefix to prevent subdomain overwrites. [src2]
- **Referer stripping breaks validation**: Privacy extensions and `Referrer-Policy: no-referrer` strip the Referer header, causing false rejections. Fix: Use Origin header as primary check; fall back to Referer; allow requests with neither header only cautiously. [src1]
- **BREACH attack on CSRF tokens**: Compression + encryption can leak CSRF token bytes. Fix: Use per-request token masking (Django and Spring do this automatically) or disable HTTP compression on pages containing tokens. [src5]

## Diagnostic Commands

> Full script: [diagnostic-commands.sh](scripts/diagnostic-commands.sh) (26 lines)

```bash
# Check SameSite attribute on cookies
curl -sI -b cookies.txt https://your-site.com/login | grep -i set-cookie
# Test CSRF protection -- submit form without token (should fail)
curl -X POST https://your-site.com/api/transfer \
  -H "Cookie: sessionid=valid_session" \
# ... (see full script)
```

## Version History & Compatibility

| Standard/Framework | Version | Status | CSRF Feature |
|---|---|---|---|
| SameSite Cookies | Chrome 80+ (Feb 2020) | Default Lax | Browser default SameSite=Lax for all cookies |
| Fetch Metadata (Sec-Fetch-*) | Chrome 76+, Firefox 90+, Safari 16.1+ | Supported | >98% global browser coverage |
| Django CsrfViewMiddleware | Django 1.2+ (current: 6.0) | Built-in, enabled by default | Synchronizer token + Origin validation + BREACH masking |
| Spring Security CSRF | Spring Security 4.0+ (current: 6.x) | Built-in, enabled by default | Synchronizer token + CookieCsrfTokenRepository + BREACH protection |
| Express csrf-csrf | v3.x | Active, recommended | Signed double-submit cookie pattern |
| Express csurf | Deprecated Sep 2022 | Archived, DO NOT USE | Known cookie tossing bypass |
| Rails protect_from_forgery | Rails 2.0+ (current: 8.x) | Built-in, enabled by default | Synchronizer token + Origin check |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| App uses session cookies for authentication | API uses stateless JWT tokens (no cookies) | Standard Authorization header validation |
| HTML forms submit state-changing requests | All requests use application/json with custom headers (non-simple) | CORS preflight provides implicit CSRF protection |
| App has both browser and API consumers | Server-to-server communication only (no browser) | Mutual TLS or API key authentication |
| Login forms need protection from login CSRF | Static read-only site with no state changes | No CSRF protection needed |

## Important Caveats

- XSS is a strictly more powerful attack than CSRF -- if an attacker has XSS, they can bypass all CSRF defenses by reading tokens from the DOM and submitting requests as the victim
- SameSite=Lax is now the browser default, which blocks most naive CSRF attacks, but sophisticated attackers can still exploit same-site cross-origin scenarios
- Mobile apps using WebView may not enforce SameSite cookies consistently -- test CSRF protection specifically in your mobile WebView environment
- Fetch Metadata headers are not sent to non-HTTPS URLs -- your site must use HTTPS for this defense to work
- Some CDNs and reverse proxies may strip Sec-Fetch-* headers -- verify they are forwarded to your application
- CSRF protection must be applied to ALL state-changing endpoints, including file uploads, settings changes, and API endpoints -- not just financial transactions

## Related Units
<!-- Generated from related_kos frontmatter -->

- [XSS Prevention](/software/security/xss-prevention/2026)
- [Session Management Patterns](/software/patterns/session-management/2026)
- [SQL Injection Prevention](/software/security/sql-injection-prevention/2026)
