Claude Skill

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

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Download jovidecroock-pracht-skills_audit-csrf-8bcdd52.zip · 3 KB

Install

skills CLI npx skills add https://github.com/JoviDeCroock/pracht/tree/main/skills/audit-csrf
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jovidecroock-pracht@llmmart
Git 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:

  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

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

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.

No comments yet.

Reviews (0)

No reviews yet.