audit-csrf
Check CSRF posture across every form and mutation API. Pracht enforces same-origin on mutation API requests by default (`api.requireSameOrigin`); this verifies the default is intact and that cookies, middleware, or tokens cover what it does not. Use for "audit CSRF", "check CSRF
Install
npx skills add https://github.com/JoviDeCroock/pracht/tree/main/skills/audit-csrf
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jovidecroock-pracht@llmmart
git clone https://github.com/JoviDeCroock/pracht.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole jovidecroock/pracht collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Pracht Audit CSRF
What the framework guarantees: by default (ApiConfig.requireSameOrigin,
true unless explicitly disabled), the runtime rejects state-changing API
requests (POST/PUT/PATCH/DELETE) with a 403 unless the browser signals
an exact same-origin fetch (Sec-Fetch-Site: same-origin) or the request's
Origin/Referer matches the request URL's origin. same-site is not
accepted (sibling subdomains can be attacker-controlled). Requests with no
browser provenance headers at all (curl, server-to-server) are allowed — the
threat model is browser-form CSRF, which cannot strip those headers. Page
routes reject unsafe methods outright, so the API surface is where mutations
live.
The same check also covers WebSocket upgrade requests (any request carrying
an Upgrade header), even though they are GET. Browsers do not apply CORS to
WebSocket, so an upgrade is a cross-site-reachable, cookie-carrying request —
cross-site WebSocket hijacking. requireSameOrigin: false therefore opens
sockets as well as mutations.
The audit therefore targets the OPT-OUTS and the remaining layers, in order of preference:
- Built-in same-origin enforcement (
requireSameOrigin, on by default). SameSite=Lax(orStrict) on session cookies — defense in depth.- Origin-check middleware — only needed when
requireSameOriginis disabled or for non-/apiendpoints. - Per-request tokens — only when
SameSite=Noneis required.
Prerequisites: pracht inspect requires a vite config that registers the
pracht plugin.
Step 0: Check the built-in guard
Read src/routes.ts (the app manifest) for
defineApp({ api: { requireSameOrigin } }). Absent means true (the
default). An explicit requireSameOrigin: false is a top finding (error):
the project has opted out of the built-in CSRF protection and MUST show
compensating layers (origin-check middleware or token protocol) — demand them
in the report.
Step 1: Inventory mutation surfaces
Forms
Grep for <Form across src/. For each occurrence:
- Capture
method(default isget— onlypost/put/patch/deleteare CSRF-relevant). - Capture
action.
API mutations
pracht inspect api --json
MCP: when the pracht MCP server is registered (pracht dev-mcp), prefer its
inspect_routes/inspect_api/inspect_build/doctor/verify tools over
shelling out.
For each API route, read the exported methods. Mutation methods: POST,
PUT, PATCH, DELETE. Caveat: a default-export handler serves ALL
methods but reports methods: [] — check the hasDefaultHandler field
(requires a current @pracht/cli) or, on older CLIs, grep the handler file
for export default. A default handler counts as exposing all mutation
methods unless it gates on request.method itself.
WebSocket upgrades
Grep API handlers for upgrade, WebSocketPair, and status: 101. A route
that returns a handshake is a mutation-equivalent surface: it is reachable
cross-site, carries cookies, and — once open — is not covered by any
per-request check. Treat it as a mutation surface in Step 5, and additionally
verify:
- The handler authenticates the handshake (session check in the handler or
in
api.middleware). The built-in origin check stops other websites; it does nothing about an unauthenticated client. - Authorization is re-checked per message where messages carry authority. The socket outlives the request that opened it, so a session revoked afterwards does not close it.
Step 2: Inspect the session cookie
Locate cookie issuance — typically src/server/session.ts, src/api/auth/*,
or anywhere Set-Cookie appears in a response. For every cookie set:
| Attribute | Required posture | Failure mode |
|---|---|---|
HttpOnly |
Present | XSS can steal the session |
Secure |
Present in production | Sniffable on HTTP |
SameSite |
Lax or Strict |
Cross-site sends the cookie |
Path |
Set (usually /) |
Scope confusion |
Flag any cookie missing HttpOnly, missing SameSite, or with
SameSite=None without an accompanying token check. A missing
Max-Age/Expires is info only — session cookies are legitimate and
strictly shorter-lived, not a failure.
Step 3: Origin-check middleware (only for opt-outs)
A manual origin-check middleware is only needed when requireSameOrigin is
disabled, or for mutation endpoints outside /api handled by custom code.
If Step 0 found requireSameOrigin: false, look for middleware that:
- Reads
request.headers.get('origin'). - Compares against
url.originand an allowlist. - Rejects unsafe-method requests on mismatch.
The canonical shape is in recipes-auth.md (the origin-check.ts example).
Verify the wiring: the middleware name must appear in
defineApp({ api: { middleware: [...] } }) — that single global list applies
to every API route and generated capability HTTP endpoint. There is no
per-group API middleware, and
pracht inspect api --json output has no middleware field, so the manifest is
the only place to check.
Step 4: Look for token-based CSRF
Grep for: csrf, csrfToken, hidden form fields with token values, headers
like x-csrf-token. Verify both sides of the protocol exist (issue + verify).
A token issuer with no verifier (or vice versa) is a bug.
Step 5: Score each mutation surface
For each <Form> and each mutation API, assign a severity (primary) and a
posture verdict (secondary):
info/ Strong — built-in same-origin enforcement intact (default) +SameSite=Lax/Strictcookies.info/ Adequate — built-in enforcement intact; cookie posture unknown orSameSiteunset (the runtime check still blocks browser CSRF).warn/ Compensated —requireSameOrigin: falsebut a verified origin-check middleware or token protocol covers the surface.warn/ Token-only — token verified, cookie hasSameSite=None.error/ Weak —requireSameOrigin: falseand no token / no origin-check.warn/ Unknown — cookie source not located — investigate.
Produce:
| Surface | File:Line | Method | requireSameOrigin | Cookie posture | Middleware/Token? | Severity | Verdict |
|---|
Rules
- Point users at https://pracht.resynapse.dev/docs/recipes/auth for cookie
and middleware patterns, and at
ApiConfig.requireSameOriginfor the built-in guard. - Do not flag GET-only forms or read-only API methods.
- Remember: a hydrated
<Form>intercepts unsafe-method submissions and issuesfetch(actionUrl, { method, body: formData })— not a document POST. Fetch's default credentials mode issame-origin, so a cross-originactiongets no cookies at all; same-origin submissions carry cookies and the browser'sSec-Fetch-Site/Originheaders, which the built-in check validates. - If the project sets
SameSite=None, require either a token check or an origin-check in addition to the built-in guard — explain why in the report. - Do not auto-fix. CSRF strategy is a policy decision; surface the gaps and let the user choose layers.
$ARGUMENTS
Files (pracht)
-
SKILL.md 7.8 KB
--- name: audit-csrf version: 1.2.1 description: | Check CSRF posture across every form and mutation API. Pracht enforces same-origin on mutation API requests by default (`api.requireSameOrigin`); this verifies the default is intact and that cookies, middleware, or tokens cover what it does not. Use for "audit CSRF", "check CSRF protection", "are forms safe", "review session security". allowed-tools: - Bash - Read - Grep - Glob --- # Pracht Audit CSRF What the framework guarantees: by default (`ApiConfig.requireSameOrigin`, `true` unless explicitly disabled), the runtime rejects state-changing API requests (`POST`/`PUT`/`PATCH`/`DELETE`) with a 403 unless the browser signals an exact same-origin fetch (`Sec-Fetch-Site: same-origin`) or the request's `Origin`/`Referer` matches the request URL's origin. `same-site` is not accepted (sibling subdomains can be attacker-controlled). Requests with no browser provenance headers at all (curl, server-to-server) are allowed — the threat model is browser-form CSRF, which cannot strip those headers. Page routes reject unsafe methods outright, so the API surface is where mutations live. The same check also covers **WebSocket upgrade requests** (any request carrying an `Upgrade` header), even though they are `GET`. Browsers do not apply CORS to WebSocket, so an upgrade is a cross-site-reachable, cookie-carrying request — cross-site WebSocket hijacking. `requireSameOrigin: false` therefore opens sockets as well as mutations. The audit therefore targets the OPT-OUTS and the remaining layers, in order of preference: 1. **Built-in same-origin enforcement** (`requireSameOrigin`, on by default). 2. **`SameSite=Lax` (or `Strict`) on session cookies** — defense in depth. 3. **Origin-check middleware** — only needed when `requireSameOrigin` is disabled or for non-`/api` endpoints. 4. **Per-request tokens** — only when `SameSite=None` is required. Prerequisites: `pracht inspect` requires a vite config that registers the pracht plugin. ## Step 0: Check the built-in guard Read `src/routes.ts` (the app manifest) for `defineApp({ api: { requireSameOrigin } })`. Absent means `true` (the default). An explicit `requireSameOrigin: false` is a top finding (`error`): the project has opted out of the built-in CSRF protection and MUST show compensating layers (origin-check middleware or token protocol) — demand them in the report. ## Step 1: Inventory mutation surfaces ### Forms Grep for `<Form ` across `src/`. For each occurrence: - Capture `method` (default is `get` — only `post`/`put`/`patch`/`delete` are CSRF-relevant). - Capture `action`. ### API mutations ```bash pracht inspect api --json ``` MCP: when the pracht MCP server is registered (`pracht dev-mcp`), prefer its `inspect_routes`/`inspect_api`/`inspect_build`/`doctor`/`verify` tools over shelling out. For each API route, read the exported `methods`. Mutation methods: `POST`, `PUT`, `PATCH`, `DELETE`. Caveat: a `default`-export handler serves ALL methods but reports `methods: []` — check the `hasDefaultHandler` field (requires a current `@pracht/cli`) or, on older CLIs, grep the handler file for `export default`. A default handler counts as exposing all mutation methods unless it gates on `request.method` itself. ### WebSocket upgrades Grep API handlers for `upgrade`, `WebSocketPair`, and `status: 101`. A route that returns a handshake is a mutation-equivalent surface: it is reachable cross-site, carries cookies, and — once open — is not covered by any per-request check. Treat it as a mutation surface in Step 5, and additionally verify: - The handler **authenticates the handshake** (session check in the handler or in `api.middleware`). The built-in origin check stops other *websites*; it does nothing about an unauthenticated client. - Authorization is re-checked per message where messages carry authority. The socket outlives the request that opened it, so a session revoked afterwards does not close it. ## Step 2: Inspect the session cookie Locate cookie issuance — typically `src/server/session.ts`, `src/api/auth/*`, or anywhere `Set-Cookie` appears in a response. For every cookie set: | Attribute | Required posture | Failure mode | | ---------------- | ---------------------------- | --------------------------- | | `HttpOnly` | Present | XSS can steal the session | | `Secure` | Present in production | Sniffable on HTTP | | `SameSite` | `Lax` or `Strict` | Cross-site sends the cookie | | `Path` | Set (usually `/`) | Scope confusion | Flag any cookie missing `HttpOnly`, missing `SameSite`, or with `SameSite=None` without an accompanying token check. A missing `Max-Age`/`Expires` is `info` only — session cookies are legitimate and strictly shorter-lived, not a failure. ## Step 3: Origin-check middleware (only for opt-outs) A manual origin-check middleware is only needed when `requireSameOrigin` is disabled, or for mutation endpoints outside `/api` handled by custom code. If Step 0 found `requireSameOrigin: false`, look for middleware that: - Reads `request.headers.get('origin')`. - Compares against `url.origin` and an allowlist. - Rejects unsafe-method requests on mismatch. The canonical shape is in `recipes-auth.md` (the `origin-check.ts` example). Verify the wiring: the middleware name must appear in `defineApp({ api: { middleware: [...] } })` — that single global list applies to every API route and generated capability HTTP endpoint. There is no per-group API middleware, and `pracht inspect api --json` output has no middleware field, so the manifest is the only place to check. ## Step 4: Look for token-based CSRF Grep for: `csrf`, `csrfToken`, hidden form fields with token values, headers like `x-csrf-token`. Verify both sides of the protocol exist (issue + verify). A token issuer with no verifier (or vice versa) is a bug. ## Step 5: Score each mutation surface For each `<Form>` and each mutation API, assign a severity (primary) and a posture verdict (secondary): - `info` / **Strong** — built-in same-origin enforcement intact (default) + `SameSite=Lax`/`Strict` cookies. - `info` / **Adequate** — built-in enforcement intact; cookie posture unknown or `SameSite` unset (the runtime check still blocks browser CSRF). - `warn` / **Compensated** — `requireSameOrigin: false` but a verified origin-check middleware or token protocol covers the surface. - `warn` / **Token-only** — token verified, cookie has `SameSite=None`. - `error` / **Weak** — `requireSameOrigin: false` and no token / no origin-check. - `warn` / **Unknown** — cookie source not located — investigate. Produce: | Surface | File:Line | Method | requireSameOrigin | Cookie posture | Middleware/Token? | Severity | Verdict | | ------- | --------- | ------ | ----------------- | -------------- | ----------------- | -------- | ------- | ## Rules 1. Point users at <https://pracht.resynapse.dev/docs/recipes/auth> for cookie and middleware patterns, and at `ApiConfig.requireSameOrigin` for the built-in guard. 2. Do not flag GET-only forms or read-only API methods. 3. Remember: a hydrated `<Form>` intercepts unsafe-method submissions and issues `fetch(actionUrl, { method, body: formData })` — not a document POST. Fetch's default credentials mode is `same-origin`, so a cross-origin `action` gets no cookies at all; same-origin submissions carry cookies and the browser's `Sec-Fetch-Site`/`Origin` headers, which the built-in check validates. 4. If the project sets `SameSite=None`, require either a token check or an origin-check in addition to the built-in guard — explain why in the report. 5. Do not auto-fix. CSRF strategy is a policy decision; surface the gaps and let the user choose layers. $ARGUMENTS
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.