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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- csrf-protection Installs to ~/.agents/skills/csrf-protection/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install csrf-protection The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project csrf-protection Installs to .agents/skills/csrf-protection/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/csrf-protection.zipunzip -o csrf-protection.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) csrf-protection PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- Your problem is which origins may read API responses: use cors-configuration
- You need overall session cookie design: use session-auth
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.
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 authenticated | CSRF risk | Action |
|---|---|---|
| Session cookie, any SameSite value | Yes | Token or origin check on every unsafe method |
Authorization: Bearer from JS memory, no cookies | No (browser does not attach it) | No CSRF token needed; watch for XSS instead |
| Bearer token stored in a cookie and read server-side | Yes | Treat as cookie auth |
| HTTP Basic / client TLS cert | Yes | Treat as cookie auth |
| Server-to-server API key | No | Exempt 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.
Recommended defence per stack
| Stack | Default | Notes |
|---|---|---|
| Django | CsrfViewMiddleware (on by default) | Never @csrf_exempt a session-auth view. Set CSRF_TRUSTED_ORIGINS with schemes (https://app.example.com) behind proxies |
| Rails | protect_from_forgery with: :exception (default in ApplicationController) | skip_forgery_protection only on token-auth API controllers inheriting from ActionController::API |
| Laravel | CSRF middleware in the web group | Exclusions via $middleware->validateCsrfTokens(except: [...]) in Laravel 11+; keep the list tiny |
| Spring Security | Enabled by default | For SPAs use CookieCsrfTokenRepository.withHttpOnlyFalse() plus the SPA token request handler from the Spring docs; do not call csrf().disable() on cookie sessions |
| Express | csrf-csrf (signed double submit) or csrf-sync (synchronizer) | csurf is deprecated and archived; replace it |
| Next.js Server Actions | Built-in Origin vs Host comparison, POST only | Behind a proxy that rewrites Host, set experimental.serverActions.allowedOrigins; Route Handlers get no automatic protection |
| Go 1.25+ | http.NewCrossOriginProtection() wrapping the mux | Uses Sec-Fetch-Site with Origin fallback; add trusted origins with AddTrustedOrigin |
| Older Go | gorilla/csrf or a Fetch Metadata middleware |
Token patterns, when you must build one
- Synchronizer token (stateful): random 128-bit token stored in the server session,
rendered into forms or a meta tag, compared with
hmac.compare_digeston unsafe methods. Simplest correct choice for server-rendered apps. - 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. - 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-TOKENon same-origin requests only. - Multipart uploads and
fetchwithkeepaliveneed 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/plainPOSTs 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: truewith 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.yaml
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.