# CSRF tokens on state-changing requests

- **Pattern:** `ab-002376` (`security-hardening.auth-session.csrf-protection`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002376
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002376)

## Why it matters

Cross-site request forgery (CWE-352) allows a malicious page to trigger authenticated state-changing requests in a victim's browser using their existing session cookie. Without CSRF protection, any site can silently transfer funds, change email addresses, or delete accounts on behalf of a logged-in user. OWASP A01 (Broken Access Control) ranks CSRF among the most exploited web vulnerabilities. NIST 800-53 SC-23 requires session authenticity protection. The attack requires no special privileges — a single malicious link is sufficient. Cookies with `SameSite=Lax` or `Strict` are the minimal defense; APIs accepting cookie-authenticated requests need explicit origin validation on top.

## Severity rationale

High because a successful CSRF attack lets any website perform authenticated actions as the victim without their knowledge, with no interaction beyond visiting a page.

## Remediation

For Next.js apps, validate the `Origin` header on every state-changing route and ensure cookies are `SameSite=Lax` at minimum:

```ts
export function validateOrigin(req: Request): boolean {
  const origin = req.headers.get('origin')
  const host = req.headers.get('host')
  if (!origin || !host) return false
  return new URL(origin).host === host
}
```

For richer CSRF protection, use the `csrf-csrf` double-submit cookie pattern in `src/middleware.ts`. Do not rely on `SameSite=None` — it provides zero CSRF protection.

## Detection

- **ID:** `csrf-protection`
- **Severity:** `high`
- **What to look for:** Count every state-changing endpoint (POST, PUT, PATCH, DELETE routes) in the application. For each, check how state-changing requests (POST, PUT, PATCH, DELETE) are protected against cross-site request forgery. Look for CSRF token middleware (csurf, csrf-csrf, edge-csrf), SameSite cookie attributes, custom request header validation (e.g., checking Origin or X-Requested-With headers), or framework-provided CSRF protection. Note that SameSite=Strict or SameSite=Lax on session cookies provides meaningful CSRF protection for most use cases.
- **Pass criteria:** State-changing API endpoints are protected by at least one of: CSRF tokens validated server-side, SameSite=Strict or SameSite=Lax cookie attribute on auth cookies, custom request header requirement (X-Requested-With or similar), or origin validation — 100% of state-changing endpoints must have CSRF protection. Report: "X state-changing endpoints found, all Y protected with CSRF tokens or SameSite cookies."
- **Fail criteria:** POST/PUT/PATCH/DELETE routes accept session-cookie-authenticated requests with no CSRF protection, and cookies are not set with SameSite=Strict or SameSite=Lax.
- **Skip (N/A) when:** The application uses token-based auth (JWT in Authorization header, not cookies) where CSRF is not applicable. Or the project is an API with no browser-facing endpoints.
- **Detail on fail:** Identify the exposed routes. Example: `"POST /api/transfer and POST /api/settings accept cookie-authenticated requests without CSRF tokens; cookies lack SameSite attribute"` or `"Auth cookies set with SameSite=None — CSRF attacks possible from any origin"`
- **Remediation:** The simplest approach for most Next.js or similar apps is to ensure session cookies use SameSite=Lax (the browser default for modern browsers, but should be explicit) combined with Origin header validation:

  ```ts
  // Validate origin for state-changing requests
  export function validateOrigin(req: Request): boolean {
    const origin = req.headers.get('origin')
    const host = req.headers.get('host')
    if (!origin || !host) return false
    return new URL(origin).host === host
  }

  // Or use the csrf-csrf library for double-submit cookie pattern
  import { doubleCsrf } from 'csrf-csrf'

  const { generateToken, validateRequest } = doubleCsrf({
    getSecret: () => process.env.CSRF_SECRET!,
    cookieName: '__Host-csrf',
    cookieOptions: { sameSite: 'strict', secure: true },
  })
  ```

## External references

- cwe CWE-352
- owasp A01
- nist SC-23

Taxons: access-control

HTML version: https://auditbuffet.com/patterns/ab-002376
