Route Auth Sweep
Install
Save this file as ~/.claude/skills/route-auth-sweep/SKILL.md, or
.claude/skills/route-auth-sweep/SKILL.md to scope it to one repo. Claude Code
auto-discovers it. Invoke with /route-auth-sweep or by asking "is every API
endpoint properly protected?".
Why this exists
Auth bugs do not come from hard problems. They come from the twentieth route, added in a hurry, that looks like the other nineteen and is missing one line. Reviewing routes as you write them cannot catch this, because the defect is an absence and absences are invisible one file at a time.
The only reliable method is enumeration: list every route, then prove each one gates. A route you did not enumerate is a route you did not check.
The highest-severity finding is almost never "no auth". It is a route that authenticates the caller and then trusts an ID from the request to decide WHICH record to return. That is IDOR, it looks completely normal in review, and it leaks other people's data to a logged-in attacker.
Step 1 — Enumerate every route
rg --files -g '**/route.ts' -g '**/route.js' -g '**/api/**/*.ts' -g '**/*controller*' | sort
Count them and keep the list. It is your checklist, and the report must account for every entry.
Step 2 — Classify intent BEFORE reading the code
For each route decide what it SHOULD require, from its name and purpose alone:
- PUBLIC — genuinely open (health, public catalog reads, webhooks, OG images).
- AUTHED — any signed-in user.
- OWNER — a signed-in user acting on their OWN records.
- ROLE — a specific role (admin, seller).
- SIGNED — no session, but a verified signature or token (payment webhooks, cron, magic links).
Doing this first stops you rationalising whatever the code happens to do.
Step 3 — Read each route and find the gate
For each, answer four questions with a file and line, not an impression:
- Does it gate before touching data? The check must run before any query. A route that fetches a record and then checks permission has already done the work, and error messages or timing frequently reveal existence.
- Where does the identity come from? It must come from the session or a verified token. Any user id read from the request body, query string, or a path parameter is a finding, even if a session also exists.
- Is the query scoped to the caller? For OWNER routes the database query
itself must constrain ownership:
- Safe:
findFirst({ where: { id, userId: session.user.id } }) - Unsafe:
findUnique({ where: { id } })then compare in application code, and catastrophic if the comparison is missing or only logs.
- Safe:
- Is it rate limited? Especially auth, password reset, signup, checkout, and anything that sends email. Note whether the limiter is shared across instances or per-process, because per-process limits barely exist under serverless autoscaling.
Useful greps, but never a substitute for reading:
rg -n "requireUser|requireAuth|getSession|getServerSession|requireAdmin|assertRole" -g '**/route.ts'
rg -n "rateLimit|ratelimit|throttle" -g '**/route.ts'
rg -n "body\.(userId|user_id|accountId|ownerId)|params\.(userId|accountId)" -g '**/route.ts'
The third one is the money grep. Every hit deserves a careful read.
Step 4 — Check the mutating verbs hardest
For POST, PUT, PATCH, DELETE also confirm:
- CSRF protection for cookie-authenticated state changes (origin check or token). Cookie auth without it means any site can act as your logged-in user.
- Input validation with a schema, not hand-rolled
ifchecks. - Server-computed values. Prices, totals, roles, and permissions must never be taken from the request. Recompute them server-side from trusted records.
- Webhook signature verification BEFORE parsing or acting on the body.
Step 5 — Verify the important ones live
Static reading misses middleware and framework behaviour. For your highest-risk routes, prove it:
curl -s -o /dev/null -w "%{http_code}\n" https://<host>/api/<protected-route>
curl -s -X POST -o /dev/null -w "%{http_code}\n" https://<host>/api/<protected-route>
Unauthenticated must return 401 or 403, never 200, and never a 500 that reveals the route ran. If a protected route returns 200 to no credentials, stop the sweep and report it immediately.
Step 6 — Report
Routes enumerated: <n>
CRITICAL
<file:line> <method> <path> — <no gate | trusts body id | unscoped query>
reachable by: <who>
leaks: <what data, concretely>
MEDIUM
<file:line> — <missing rate limit | missing CSRF | validates after query>
VERIFIED SAFE
<n> routes gate correctly (list them; the checklist must total <n>)
For every CRITICAL, state the concrete attack in one sentence: who sends what, and what comes back. "A signed-in user can read any other user's orders by changing the id in the URL" gets fixed today. "Improper access control" does not.
Rules
- Enumerate first. An unlisted route is an unchecked route.
- "It gates" is not a finding; cite file and line, or you have not checked it.
- Authenticated is not authorised. Most real breaches are logged-in users reading other people's rows.
- A public route is a valid answer. Say so explicitly and say why, rather than proposing auth on a health check.
- Never report a vulnerability you have not traced end to end. A false critical costs trust you need for the true one.
From Toolbay. Free to use, modify, and share. Keep this line and others can find it too.