Skip to main content
Technology & EngineeringSecurity Practices247 lines

CSRF Protection

Protect web applications against cross-site request forgery (CSRF) using tokens, SameSite cookies, and origin validation.

Quick Summary18 lines
You are an expert in defending web applications against cross-site request forgery (CSRF) attacks using token-based, header-based, and cookie-based mitigations.

## Key Points

1. User authenticates on `bank.example.com` and receives a session cookie.
2. User visits `evil.example.com`, which contains a hidden form targeting `bank.example.com/transfer`.
3. The browser attaches the session cookie automatically.
4. The bank processes the forged request as if the user initiated it.
- **`Strict`**: Cookie never sent on cross-site requests. Strongest protection but breaks legitimate cross-site navigation (e.g., links from email).
- **`Lax`**: Cookie sent on top-level GET navigations but not on POST or subresource requests. Good default balance.
- **`None`**: Cookie sent on all cross-site requests (requires `Secure` flag). Offers no CSRF protection.
1. **Use SameSite=Lax as a baseline**: It stops most CSRF without breaking normal navigation. Combine with token-based protection for defense in depth.
2. **Require a custom header for APIs**: Browsers do not allow cross-origin requests to set custom headers without a preflight. Requiring `X-CSRF-Token` or similar blocks simple CSRF.
3. **Generate cryptographically random tokens**: Use `crypto.randomBytes` or equivalent, never predictable values.
4. **Validate on every state-changing request**: Apply CSRF checks to POST, PUT, PATCH, DELETE — never only on specific routes.
5. **Rotate tokens per session**: Generate a new token when a session is created or when the user authenticates.
skilldb get security-practices-skills/CSRF ProtectionFull skill: 247 lines
Paste into your CLAUDE.md or agent config

CSRF Protection — Application Security

You are an expert in defending web applications against cross-site request forgery (CSRF) attacks using token-based, header-based, and cookie-based mitigations.

Core Philosophy

CSRF protection is rooted in a single question: "Did this request genuinely come from my application, or was the user's browser tricked into sending it?" The browser automatically attaches cookies to every request for a given domain, which means authentication alone cannot distinguish between a legitimate user action and a forged one. CSRF defenses exist to close this gap by requiring proof of intent that only your own application can provide.

Defense in depth is essential because no single CSRF mitigation is bulletproof in isolation. SameSite cookies depend on browser support and do not cover every scenario. Tokens can leak through Referer headers or logging. Origin validation can be stripped by proxies. The strongest posture layers multiple defenses — SameSite cookies as a baseline, synchronizer tokens for form submissions, and custom headers for API calls — so that a weakness in one layer is covered by another.

The most effective CSRF strategies align with the principle of least surprise: state-changing operations use POST (never GET), tokens are scoped to the session and rotated on authentication, and developers never need to remember to "opt in" to protection because the middleware enforces it globally. When CSRF protection is an afterthought bolted onto individual routes, gaps are inevitable.

Anti-Patterns

  • Using GET requests for state-changing operations: Links, image tags, prefetch hints, and browser extensions can all trigger GET requests silently. Any action that modifies data — transfers, deletions, settings changes — must use POST, PUT, PATCH, or DELETE.

  • Relying solely on SameSite cookies: While SameSite=Lax is an excellent baseline, it does not protect against subdomain attacks, and older browsers ignore it entirely. Treating it as the only defense leaves gaps that an attacker can exploit.

  • Embedding CSRF tokens in URLs: Tokens placed in query strings leak through Referer headers, browser history, server logs, and analytics tools. Always transmit tokens in POST bodies or custom request headers.

  • Exempting AJAX routes from CSRF validation: Developers sometimes assume that because AJAX requests require JavaScript, they cannot be forged cross-origin. This is false if CORS is misconfigured or if the attacker uses a form submission that mimics the API call format.

  • Sharing CSRF tokens across sessions or users: A CSRF token must be tied to a specific session. Reusing tokens across sessions, caching them in shared layers, or failing to regenerate them after login undermines the entire mechanism.

Overview

CSRF exploits the trust a server places in a user's browser. If a user is authenticated, an attacker can trick their browser into sending forged requests (form submissions, API calls) to the target site. The server processes them as legitimate because the session cookie is automatically attached. Defenses center on verifying that a request genuinely originated from your own site.

Core Concepts

How CSRF Works

  1. User authenticates on bank.example.com and receives a session cookie.
  2. User visits evil.example.com, which contains a hidden form targeting bank.example.com/transfer.
  3. The browser attaches the session cookie automatically.
  4. The bank processes the forged request as if the user initiated it.

Defense Layers

DefenseMechanism
Synchronizer TokenUnique per-session or per-request token in forms
Double-Submit CookieToken in cookie + request body/header must match
SameSite Cookie AttributeBrowser refuses to send cookie on cross-site requests
Origin/Referer ValidationServer checks Origin or Referer header
Custom Request HeadersAPIs require a custom header that forms cannot set

SameSite Cookie Values

  • Strict: Cookie never sent on cross-site requests. Strongest protection but breaks legitimate cross-site navigation (e.g., links from email).
  • Lax: Cookie sent on top-level GET navigations but not on POST or subresource requests. Good default balance.
  • None: Cookie sent on all cross-site requests (requires Secure flag). Offers no CSRF protection.

Implementation Patterns

Synchronizer Token Pattern (Express + csurf Alternative)

const crypto = require('crypto');

// Generate and store a CSRF token in the session
function generateCsrfToken(session) {
  const token = crypto.randomBytes(32).toString('hex');
  session.csrfToken = token;
  return token;
}

// Middleware to validate CSRF token
function csrfProtection(req, res, next) {
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
    return next();
  }

  const token = req.body._csrf
    || req.headers['x-csrf-token']
    || req.query._csrf;

  if (!token || token !== req.session.csrfToken) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }

  next();
}

// In the route handler, embed the token in forms
app.get('/transfer', (req, res) => {
  const token = generateCsrfToken(req.session);
  res.render('transfer', { csrfToken: token });
});

app.use(csrfProtection);
<!-- In the form template -->
<form method="POST" action="/transfer">
  <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
  <input type="text" name="amount" />
  <button type="submit">Transfer</button>
</form>

Double-Submit Cookie Pattern

const crypto = require('crypto');

// Set a CSRF cookie (not HttpOnly so JS can read it)
function setCsrfCookie(res) {
  const token = crypto.randomBytes(32).toString('hex');
  res.cookie('csrf-token', token, {
    sameSite: 'Strict',
    secure: true,
    httpOnly: false,   // client JS must read this
    path: '/',
  });
  return token;
}

// Middleware: compare cookie value to header value
function doubleSubmitCheck(req, res, next) {
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
    return next();
  }

  const cookieToken = req.cookies['csrf-token'];
  const headerToken = req.headers['x-csrf-token'];

  if (!cookieToken || !headerToken || cookieToken !== headerToken) {
    return res.status(403).json({ error: 'CSRF validation failed' });
  }
  next();
}
// Client-side: read cookie and attach header
function getCookie(name) {
  const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
  return match ? decodeURIComponent(match[1]) : null;
}

fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': getCookie('csrf-token'),
  },
  body: JSON.stringify({ amount: 100 }),
});

SameSite Cookie Configuration

// Express session with SameSite
app.use(session({
  secret: process.env.SESSION_SECRET,
  cookie: {
    httpOnly: true,
    secure: true,           // requires HTTPS
    sameSite: 'Lax',        // or 'Strict' for maximum protection
    maxAge: 3600000,
  },
  resave: false,
  saveUninitialized: false,
}));
# Django settings
CSRF_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = 'Lax'
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True

Origin / Referer Header Validation

function originCheck(req, res, next) {
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
    return next();
  }

  const origin = req.headers['origin'] || req.headers['referer'];
  const allowedOrigins = ['https://myapp.example.com'];

  if (!origin) {
    return res.status(403).json({ error: 'Missing origin header' });
  }

  const parsed = new URL(origin);
  if (!allowedOrigins.includes(parsed.origin)) {
    return res.status(403).json({ error: 'Invalid origin' });
  }

  next();
}

Django Built-In CSRF

# Django has CSRF middleware enabled by default
# settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',   # built-in
    # ...
]

# In templates, use the {% csrf_token %} tag:
# <form method="post">
#   {% csrf_token %}
#   ...
# </form>

# For AJAX, read the csrftoken cookie and set X-CSRFToken header
# Django checks this automatically

Best Practices

  1. Use SameSite=Lax as a baseline: It stops most CSRF without breaking normal navigation. Combine with token-based protection for defense in depth.
  2. Require a custom header for APIs: Browsers do not allow cross-origin requests to set custom headers without a preflight. Requiring X-CSRF-Token or similar blocks simple CSRF.
  3. Generate cryptographically random tokens: Use crypto.randomBytes or equivalent, never predictable values.
  4. Validate on every state-changing request: Apply CSRF checks to POST, PUT, PATCH, DELETE — never only on specific routes.
  5. Rotate tokens per session: Generate a new token when a session is created or when the user authenticates.
  6. Reject requests with missing Origin/Referer: For state-changing requests, require at least one of these headers and validate it.
  7. Do not use GET for state changes: GET requests bypass most CSRF defenses and are cached/logged/prefetched by browsers.

Common Pitfalls

  • Relying solely on SameSite cookies: Older browsers do not support SameSite. Use tokens as well.
  • Excluding AJAX routes from CSRF checks: Attackers can submit cross-origin requests with fetch or XMLHttpRequest if CORS is misconfigured. Always validate.
  • Leaking CSRF tokens in URLs: Tokens in query strings end up in server logs, Referer headers, and browser history. Use POST bodies or custom headers.
  • Not regenerating tokens after login: Session fixation can carry over a known CSRF token. Regenerate on authentication.
  • Using GET requests for actions: Links, image tags, and prefetch mechanisms can trigger GET requests silently.
  • Accepting tokens from any source without priority: Define a clear precedence (header > body > query) and reject if the token source is ambiguous.

Install this skill directly: skilldb add security-practices-skills

Get CLI access →