CSRF Protection skill for Codex

Forged requests die at the middleware. Codex first decides whether a route is exposed at all, since bearer-token APIs are not while cookie sessions always are. It then turns on the framework's own CSRF defence, hunts down exemptions and state-changing GET routes, adds an Origin and Sec-Fetch-Site guard as a second layer, and proves the fix with tests that explicitly enable CSRF checks most test clients turn off.

  • Security
  • $csrf-protection
  • 154 lines
  • 2 files
  • ~1,850 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- csrf-protection

Installs to ~/.agents/skills/csrf-protection/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $csrf-protection, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Protects cookie-authenticated web apps from cross-site request forgery using framework CSRF middleware, synchronizer or signed double-submit tokens, Origin and Sec-Fetch-Site checks, and SameSite cookies, and tells when an API is not exposed at all. Covers Django, Rails, Laravel, Spring Security, Express, Next.js Server Actions and Go net/http. Use when adding forms or state-changing endpoints, disabling CSRF for an API, debugging 403 CSRF failures, or reviewing cookie settings.

Use it when

  • Adding forms or state-changing endpoints to a cookie-authenticated app
  • Someone wants to disable CSRF checks for an API or webhook
  • Debugging CSRF token failures after a proxy or domain change
  • Reviewing SameSite and cookie attributes

Use something else when

How to check the result

A forged cross-site POST with a valid session cookie returns 403 in an integration test that explicitly enables CSRF checks, while same-origin form posts and SPA requests still succeed.

The mistake it prevents

Trusting SameSite=Lax alone, which still allows forgery from sibling subdomains and through state-changing GET routes.

~/.agents/skills/csrf-protection/SKILL.md

--- name: csrf-protection description: Protects cookie-authenticated web apps from cross-site request forgery using framework CSRF middleware, synchronizer or signed double-submit tokens,… license: MIT ---

Done well means: no state-changing request authenticated by an ambient credential (cookie, HTTP auth, client certificate) succeeds when it was triggered from another site, and the defence is the framework’s own, enabled globally, with explicit and justified exemptions only.

Decide whether CSRF applies

How the request is authenticatedCSRF riskAction
Session cookie, any SameSite valueYesToken or origin check on every unsafe method
Authorization: Bearer from JS memory, no cookiesNo (browser does not attach it)No CSRF token needed; watch for XSS instead
Bearer token stored in a cookie and read server-sideYesTreat as cookie auth
HTTP Basic / client TLS certYesTreat as cookie auth
Server-to-server API keyNoExempt the route group explicitly

SameSite is a helpful layer, not the fix: Lax still sends cookies on top-level GET navigations, same-site includes every subdomain (one takeover or XSS on blog.example.com can forge requests to app.example.com), and default-Lax is not consistent across all browsers.

Inspect first

rg -n -i "csrf|xsrf|samesite|csrf_exempt|skip_forgery_protection|protect_from_forgery|csrf\(\)\.disable|VerifyCsrfToken|ValidateCsrfToken" -g '!node_modules'
rg -n "(app|router)\.(get|post|put|patch|delete)\(" --type js --type ts | head -50

List every exemption and every GET route that mutates state. A GET /logout, GET /unsubscribe?id= or GET /approve/123 is a forgery target regardless of tokens.

StackDefaultNotes
DjangoCsrfViewMiddleware (on by default)Never @csrf_exempt a session-auth view. Set CSRF_TRUSTED_ORIGINS with schemes (https://app.example.com) behind proxies
Railsprotect_from_forgery with: :exception (default in ApplicationController)skip_forgery_protection only on token-auth API controllers inheriting from ActionController::API
LaravelCSRF middleware in the web groupExclusions via $middleware->validateCsrfTokens(except: [...]) in Laravel 11+; keep the list tiny
Spring SecurityEnabled by defaultFor SPAs use CookieCsrfTokenRepository.withHttpOnlyFalse() plus the SPA token request handler from the Spring docs; do not call csrf().disable() on cookie sessions
Expresscsrf-csrf (signed double submit) or csrf-sync (synchronizer)csurf is deprecated and archived; replace it
Next.js Server ActionsBuilt-in Origin vs Host comparison, POST onlyBehind a proxy that rewrites Host, set experimental.serverActions.allowedOrigins; Route Handlers get no automatic protection
Go 1.25+http.NewCrossOriginProtection() wrapping the muxUses Sec-Fetch-Site with Origin fallback; add trusted origins with AddTrustedOrigin
Older Gogorilla/csrf or a Fetch Metadata middleware

Token patterns, when you must build one

  1. Synchronizer token (stateful): random 128-bit token stored in the server session, rendered into forms or a meta tag, compared with hmac.compare_digest on unsafe methods. Simplest correct choice for server-rendered apps.
  2. Signed double submit (stateless): cookie and request header both carry HMAC(secret, session_id + random). The HMAC binding to the session is mandatory; a naive double submit (any matching cookie and header) is broken by cookie injection from a sibling subdomain.
  3. Custom header only (X-Requested-With, X-CSRF: 1) for JSON APIs: forces a CORS preflight cross-origin. Valid only if CORS never allows untrusted origins with credentials and the server rejects non-JSON content types.

Origin and Fetch Metadata check

A cheap, framework-agnostic layer that also covers routes someone forgot to tokenise:

SAFE = {"GET", "HEAD", "OPTIONS"}
TRUSTED = {"https://app.example.com"}

def cross_site_guard(request):
    if request.method in SAFE:
        return None
    site = request.headers.get("Sec-Fetch-Site")
    if site is not None:
        if site in ("same-origin", "none"):     # "none" = user typed URL / bookmark
            return None
        return forbidden("cross-site request")
    origin = request.headers.get("Origin")
    if origin is None or origin == "null":
        return forbidden("missing origin")        # or fall back to token check
    if origin not in TRUSTED:
        return forbidden("untrusted origin")
    return None

Allow same-site only if you trust every subdomain. Compare full origins, never with startswith or in.

Frontend wiring

  • Server-rendered forms: the framework helper ({% csrf_token %}, form_with, @csrf, Thymeleaf auto-insertion).
  • SPA with fetch: read the token from a meta tag or the readable XSRF cookie and send it as a header. Axios does this automatically for XSRF-TOKEN / X-XSRF-TOKEN on same-origin requests only.
  • Multipart uploads and fetch with keepalive need the header too; they are common gaps.

Workflow

- [ ] 1. Classify auth mechanism per route group (table above)
- [ ] 2. List exemptions and mutating GET routes
- [ ] 3. Confirm framework middleware is on and covers every unsafe method
- [ ] 4. Convert mutating GETs to POST (or require a token in the query plus re-check)
- [ ] 5. Add Origin / Sec-Fetch-Site guard as a second layer where cheap
- [ ] 6. Set cookies: Secure, HttpOnly, SameSite=Lax or Strict, __Host- prefix
- [ ] 7. Test cross-site requests are rejected and same-site ones still work

Verification

Run against a local build only:

# Cross-site form post must be rejected (403/419), not processed
curl -i -X POST https://localhost:8443/account/email \
  -H "Cookie: sessionid=$SESSION" -H "Origin: https://evil.example" \
  -H "Sec-Fetch-Site: cross-site" -d "[email protected]"

Also add an integration test per framework (Django Client(enforce_csrf_checks=True), Rails request specs with allow_forgery_protection = true, Spring MockMvc without .with(csrf())) asserting the forged request fails. Test frameworks often disable CSRF by default, so a green suite proves nothing until checks are explicitly enabled.

For a manual proof of concept, a local HTML file auto-submitting a form to the app is enough. Only target environments the user owns.

Output template

## CSRF review: <app>
Auth: <cookie session / bearer / mixed>  Framework defence: <on/off, version>
| Route | Method | Auth | Protection | Status |
|---|---|---|---|---|
| /api/payouts | POST | cookie | exempt (csrf_exempt) | VULNERABLE |
Fixes applied: <list>   Tests added: <list>

Gotchas

  • Login CSRF: an unprotected login form lets an attacker sign the victim into the attacker’s account. Protect login and logout too.
  • Content-type checks are not a defence: text/plain POSTs with JSON bodies are simple requests and skip preflight; the server must reject them if it parses them as JSON.
  • Token rotated on every request breaks multi-tab use and back buttons; per-session tokens are fine.
  • Caching a page that embeds a CSRF token at a CDN serves one user’s token to others.
  • A permissive CORS policy (Access-Control-Allow-Credentials: true with reflected origins) lets attackers read tokens; fix it with $cors-configuration.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "CSRF Protection"
  short_description: "Forged requests die at the middleware"
  default_prompt: "$csrf-protection Is our Express API vulnerable to CSRF? We use session cookies"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem