audit-auth
Skill106 starsChanged 7 months agoMITView on GitHub
Find pracht routes and API mutations that look protected but are not: missing or non-gating auth middleware and client-only checks. Use for "audit auth", "check route protection", "find unauthenticated routes", "review middleware coverage".
In this repository33 other files it gives its agents.
- AGENTS.mdAGENTS.md
- add-authSkill
- add-capabilitiesSkill
- add-contentSkill
- add-dbSkill
- add-i18nSkill
- add-imagesSkill
- add-observabilitySkill
- add-openapiSkill
- audit-a11ySkill
- audit-agent-surfaceSkill
- audit-bundlesSkill
- audit-csrfSkill
- audit-depsSkill
- audit-headersSkill
- audit-islandsSkill
- audit-loadersSkill
- audit-redirectsSkill
- audit-secretsSkill
- audit-seoSkill
- audit-shellsSkill
- configure-isgSkill
- migrate-nextjsSkill
- pracht-debugSkill
- pracht-deploySkill
- pracht-scaffoldSkill
- pracht-test-apiSkill
- pre-deploySkill
- scaffold-e2eSkill
- scaffold-testsSkill
- tune-render-modeSkill
- typed-routesSkill
- upgrade-prachtSkill
The file
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 1 is distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 53
Manifest router: read the name→file map from src/routes.ts (or the configured manifest), where middleware is registered through defineApp({ …
- Very specific: line 56
Pages router: when the Vite config sets pagesDir, the generated name "pages" resolves to the root <pagesDir>/_middleware.ts (or .tsx, .js, …
- Very specific: line 116
If only an Augmenter is present, mark as augmented-only.
- Very specific: line 144
Treat context.agent as framework-owned, read-only verified identity.
- Very specific: line 147
When a custom adapter supplies a frozen or sealed context, flag authorization helpers that read agent or middleware-added fields through …
- Very specific: line 174
Every module in src/server-islands/ (or pracht({ serverIslandsDir })) is reachable at GET /__pracht/server-island with the visitor's …
- Distinctive: line 254
Do not auto-add middleware.
- Very specific: line 255
Treat allowed composed capability reachability as transitive.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Pracht Audit Auth
What the framework guarantees: middleware runs wrap-around style — every
middleware must return a Response (the runtime throws if it doesn't), either
return next() to continue the chain or a short-circuit Response to stop it.
The redirect() helper from @pracht/core returns a scheme/CRLF-validated
redirect Response. What the framework does NOT decide is which routes get
an auth gate — that is app wiring, and this skill audits it.
The pracht auth pattern (see https://pracht.resynapse.dev/docs/recipes/auth):
middleware loads the session onto context.session and short-circuits with a
redirect when there is no user; loaders downstream read context.session.
@pracht/session is the first-party implementation. Two of its exports map
straight onto the Gate/Augmenter classification below — requireSession() is
a Gate, sessionMiddleware() is an Augmenter — so a project using it can be
classified from the factory name without reading the middleware body. A
project that hand-rolls its session instead is not automatically wrong, but
check it for the things the package handles: an expiry inside the signed
payload (not only Max-Age), a constant-time or crypto.subtle.verify
signature check, HttpOnly/Secure/SameSite, and encryption if the cookie
carries anything beyond an opaque id.
Prerequisites: pracht inspect requires a vite config that registers the
pracht plugin.
Step 1: Identify the auth middleware(s)
pracht inspect routes --jsonMCP: if pracht dev-mcp is registered, prefer its inspect_*, doctor, and verify tools to the CLI.
inspect reports middleware names, not files. Resolve them according to the
app's router mode, then read each middleware file and classify it:
- Manifest router: read the name→file map from
src/routes.ts(or the configured manifest), where middleware is registered throughdefineApp({ middleware: { auth: () => import("./middleware/auth.ts") } }). - Pages router: when the Vite config sets
pagesDir, the generated name"pages"resolves to the root<pagesDir>/_middleware.ts(or.tsx,.js, or.jsx). It applies to every page route and never wraps API routes. There is nosrc/routes.tsmanifest to inspect unless the app has ejected. Follow imports and re-exports into underscore-reserved helpers such as<pagesDir>/_server/auth.ts; pracht excludes those helpers from client route/shell registries, but a direct import from client code still bundles them and should be treated as a server-code leak.
Then classify each resolved middleware module:
- Gate — on auth failure, returns a short-circuit
Response(redirect("/login", { request }), or a 401/403Response) WITHOUT callingnext(); on success,return next().requireSession()from@pracht/sessionis one. - Augmenter — puts user info on
context(or, in older code, on request headers), then always returnsnext(). Never short-circuits.sessionMiddleware()is one. - Other — non-auth middleware (rate limit, logging, CORS, etc.).
Flag any middleware that writes identity onto args.request.headers: the
client controls request headers, so a loader reading x-user-id back out is
trusting attacker-supplied input, and the write throws outright on Cloudflare
Workers where the incoming Request is immutable. Identity belongs on
context.
The "Augmenter" category is the silent killer: it makes loaders think
auth is enforced because request.headers.get('x-user-id') returns a value
when present, but unauthenticated requests just get null and the loader has
to handle it. Flag every loader downstream of an Augmenter that doesn't.
A shell loader that redirects anonymous users is not a gate either. A
client that already holds the shell's data sends x-pracht-shell-data: <shell>
on route-state requests and the server skips the shell loader, so the route's
data comes back without it running. Flag any shell whose loader is the only
thing between an anonymous request and the routes under it; the gate belongs
in middleware.
Step 2: Identify protected routes
A route is "expected protected" if any of:
- It has
auth/session/requireUser/similar middleware applied. - Its loader reads
context.session,getSession, or (legacy)x-user-id/x-user-email. - It lives under conventional protected paths:
/dashboard*,/admin*,/account*,/settings*,/app*(ask the user to confirm the convention if unclear). - The user has flagged it explicitly.
Build a list of expected-protected routes.
Step 3: Check coverage per protected route
For each expected-protected route:
- From
pracht inspect routes --json, read the resolvedmiddlewarearray. - Confirm at least one Gate middleware is present.
- Confirm the gate runs before any other middleware that depends on identity (order matters).
- If only an Augmenter is present, mark as
augmented-only. - For pages-router routes, inspect
rendertoo. A root pages Gate on anssgorisgroute does not protect the static document per visitor: document middleware runs during build/revalidation with a sanitized request, while later route-state requests are separate live requests. Unless an independently verified platform/CDN edge gate protects the document, report the route aserror/unprotected-staticand recommendssr/spafor session-gated pages.
Step 4: Check the API surface
Mutation endpoints (POST, PUT, PATCH, DELETE) are the highest-impact
target. From pracht inspect api --json:
- Each API route reports
path,file,methods, andhasDefaultHandler(the last requires a current@pracht/cli). Adefault-export handler serves ALL methods but reportsmethods: []— treathasDefaultHandler: trueas "every method exposed". On older CLIs where the field is missing, grep the handler file forexport defaultinstead. - For each mutation handler (named method export or default handler) and each
HTTP- or remote-MCP-exposed capability, check whether
defineApp({ api: { middleware } })applies a Gate, OR the handler/capability reads and validates a session itself. App-level API middleware wraps generated capability endpoints before capability-specific middleware. - For remote MCP, cookie-bearing transport requests are rejected before
capability dispatch and only
Authorizationis forwarded. Flag MCP-exposed capabilities whose gate depends on a browser session cookie or a custom credential header that the projection does not carry. - Treat
context.agentas framework-owned, read-only verified identity. Flag middleware or capability code that attempts to mutate or replace it instead of deriving application authorization state on a separate context field. - When a custom adapter supplies a frozen or sealed context, flag authorization
helpers that read
agentor middleware-added fields throughthis. The framework binds private-field methods to the immutable source receiver, which cannot observe fields added on its extensible overlay. Callable fields keep their own API and arrays keep their brand. Application-definedSymbol.toStringTagbranding does not affect whether an ordinary context can be overlaid, but immutable native built-ins such asMapandDatefail closed because an overlay cannot preserve their internal slots. Use a fresh mutable wrapper when a context needs native built-ins or when receiver-bound helpers depend on request state. - Inspect every HTTP-, WebMCP-, or MCP-exposed capability body for
invokeCapability(). Direct composition never re-applies app-level API middleware. Remote MCP additionally re-applies the callee'sagentPolicyand refuses destructive callees unless the tool being served is itself a destructive capability that already cleared prepare/commit — a request-scoped grant over every destructive callee, private ones included, so audit a confirmed destructive tool's body the way you would a confirmed HTTP endpoint's. Private non-destructive capabilities stay composable and rely on their named middleware for authorization. For HTTP/WebMCP composition, flag sensitive callees whose required transport authorization or approval is absent from the composing capability and the callee's named middleware. - Common bug: dashboard route is protected by middleware, but
POST /api/itemsis not — attacker bypasses the UI entirely.
Server islands
Every module in src/server-islands/ (or pracht({ serverIslandsDir })) is
reachable at GET /__pracht/server-island with the visitor's cookies, for any
page whose route or shell lists it in export const serverIslands = [...] —
with that route's middleware
(see https://pracht.resynapse.dev/docs/server-islands#security). Map each
server island to the routes that list it, directly or through their shell.
- Flag a server island listed by both gated and ungated routes whose loader
returns user-specific data without checking
contextitself —error: the ungated route serves it to anyone. - Flag pages that gate a server island only at render time
(
{isAdmin && <AdminStats />}behind a loader check, not middleware) —error: the endpoint runs it for anyone who passes that route's middleware. - Flag server island loaders that pick whose data to load from
props(userId,orderId,accountId) — the caller controls the query string (error, IDOR). Identity must come fromcontext. - Flag server island loaders with side effects (writes, sends) — the endpoint is
a
GET(warn).
Step 5: Client/server enforcement parity
Grep client components for patterns like if (!user) return <Login />. For
each occurrence, confirm that the data path is also gated server-side.
Client-side gating without a server gate is purely cosmetic and a common
source of "I see the data flash before redirect" or worse, leaked data via
SPA route loaders.
Step 6: Session cookie sanity
With @pracht/session, check the configuration rather than the mechanics:
cookie.secrets read from serverEnv (never a literal), the storage built
inside a function rather than at module scope (Workers env is request-scoped),
a __Host- cookie name unless subdomain sharing is required, and sameSite
matching the app's embedding needs. A store is required for logout to
invalidate a session anywhere other than the browser that asked.
Then check the login path for session.regenerate(), called after
credentials verify and before the user is written onto the session. Its
absence is session fixation: with a store, an attacker who can plant a cookie
for the host keeps a valid pointer to the session that becomes authenticated.
Flag it as error/fixation on any store-backed app; on a cookie-only app it
is info (the cookie carries the sealed data, not a pointer), but still worth
adding before the app grows a store. Audit every other privilege change the
same way — 2FA completion, role assumption, impersonation.
Cross-reference with audit-csrf: the same cookies that authorize the user
are the CSRF target. Recommend running audit-csrf after this skill.
Step 7: Report
| Route/API | Expected | Resolved middleware | Gate present? | Severity | Verdict |
Severity is the primary scale; the verdict is a secondary domain label:
error/unprotected— no auth middleware on a route the user expects protected.error/inconsistent— UI route is gated; sibling API is not.error/unprotected-static— a pages-router SSG/ISG document relies on request/session middleware without an independent per-request edge gate.warn/augmented-only— middleware reads session but never blocks; loader must handle null user.warn/client-only— server allows; client hides UI.error/fixation— store-backed session, noregenerate()at login.info/protected— gate confirmed.info/public-by-design— deliberately exposed (login, signup, marketing).
Rules
- The framework's
pracht inspect routes --jsonandpracht inspect api --jsonare the source of truth — group inheritance is already resolved. - Recognize Gates by behavior (short-circuits with a
Responsewithout callingnext()on failure), not by filename — projects useauth.ts,requireUser.ts,session.ts, etc. - An Augmenter is a valid pattern when paired with a separate Gate or a loader that explicitly handles the unauthenticated case. Flag it; don't condemn it.
- Public routes deliberately exposed (login, signup, marketing) should be listed but not flagged.
- Do not auto-add middleware. Auth wiring is policy.
- Treat allowed composed capability reachability as transitive. MCP blocks
destructive callees unless the served tool already cleared its own
confirmation gate, and re-applies
agentPolicy; named middleware remains the authorization seam for private non-destructive composition. Audit events identify every nested attempt withtransport: "server"and trusted request provenance invia, but observability is not an authorization gate.
$ARGUMENTS
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

