Audit authorization logic across the codebase: RBAC/ABAC role definitions, row-level security, multi-tenant data isolation, IDOR exposure, privilege escalation paths, and the presence of a default-deny posture. Every finding gets a severity, a file:line reference, and a concrete remediation step. The review is static and code-driven — no live requests, no live credentials.
A surface-level "auth check is present" observation is not a finding. The goal is to confirm that every entry point, every HTTP verb, and every DB query is bounded by the authenticated user's identity — and that a default-deny fallback protects anything that is not explicitly covered.
When to use
A PR adds or modifies middleware, guards, decorators, or policy files that control access.
A new resource type (API route, DB table, file store) is introduced without visible permission checks.
A security review or audit explicitly calls for authorization analysis.
You spot a req.user / session.userId reference used as a filter but not validated against the resource owner.
Multi-tenant features are being added or refactored.
Row-level security (RLS) policies are being added, removed, or bypassed in a migration.
When not to use
The task is purely about authentication (login, token issuance, MFA) — use a dedicated authn review.
The codebase has no server-side logic (static site, CDN-only delivery).
You need penetration testing with live requests — this skill is static/code analysis only.
The narrower 47-security-and-secrets-review already covers secrets and CORS; this skill focuses on authorization logic.
Procedure
Map the permission model. Read role/permission definitions: look for roles, permissions, policies, guards, casl, casbin, oso, or hand-rolled arrays. Note whether it is RBAC (role-based), ABAC (attribute-based), or ad-hoc. Record where the source of truth lives — a DB table, a JWT claim, a policy file, or a config object.
Enumerate entry points. List every HTTP route, GraphQL resolver, RPC handler, WebSocket event, and background job that touches user data. Use rg to find route registrations, then cross-reference against middleware chains to confirm each entry point has an auth check before the handler body runs.
Check row-level isolation. For every DB query that returns potentially sensitive records, verify the WHERE clause includes tenant_id, org_id, or owner_id tied to the authenticated user — not just passed in from the request body. In ORMs like Prisma/TypeORM, check that base scopes or default scopes include the tenant filter everywhere, not just in the obvious list endpoints.
Hunt for IDOR. Search for patterns where an ID from the request (path param, query string, body) is used directly in a lookup without an ownership assertion. findById(req.params.id) without AND owner_id = req.user.id is a textbook IDOR. Check every findOne, findById, getOne, and select ... where id = $1 call.
Verify default-deny. Confirm that a route/resource without an explicit allow rule is blocked, not allowed. Check the fallback/catch-all handler. Look for allowAll(), skipAuth(), @Public(), or isPublic: true annotations and confirm they are intentional, minimal, and documented with a comment stating why the route is public.
Inspect privilege escalation paths. Look for self-service role assignment endpoints: can a regular user set role: "admin" in an update payload? Are role fields stripped before upsert? Check PATCH /users/:id and PUT /profile for unguarded role, permissions, or isAdmin fields.
Audit cross-tenant operations. In multi-tenant apps, check admin/support routes that bypass tenant filters. Confirm they require an explicit elevated role checked in server-side middleware (not just in the UI), and that access is logged with actor, target tenant, and timestamp.
Verify denied path responses. Confirm that unauthorized access returns 403 Forbidden (not 404 leaking resource existence, not 200 with empty data). Check that error messages do not echo tenant IDs, user IDs, or internal role names back to the caller.
Check HTTP verb coverage. A route that guards GET /resource/:id with ownership check does not automatically guard PUT /resource/:id or DELETE /resource/:id. Verify all verbs are covered independently.
Check test coverage for denied paths. Look for tests that assert denied behavior: a user accessing another user's resource should return 403, not 200 or 404. Flag any sensitive resource that has no test exercising the denied path.
Concrete checks
Every route has auth middleware registered before the handler, not after (check middleware order explicitly in Express/Koa/Fastify chains).
Row-level queries always include an owner/tenant filter derived from the verified session, not from the request payload.
No IDOR: resource IDs from the request are cross-checked against the authenticated user's identity at the query layer, not just in the handler return.
Role assignment requires elevated privilege; regular users cannot self-promote via an unguarded field in an update body.
Default posture is deny; allow-lists are explicit, narrow, and annotated with a reason.
Cross-tenant admin endpoints are gated on a server-side role check and emit an access log entry.
Unauthorized access returns HTTP 403 with no resource detail in the response body.
All HTTP verbs on a resource (GET, POST, PUT, PATCH, DELETE) are independently guarded — not just the read path.
Unit/integration tests cover at least one denied path per sensitive resource.
ABAC policies reference immutable server-side attributes, not user-supplied request fields.
Soft-deleted records are excluded from ownership-bypassed direct lookups (not just from normal list queries).
JWT role claims are re-validated server-side on each request; a changed role in the JWT does not grant elevated access until re-verified.
Commands
# Find route registrations (Express/Koa style)
rg -n "router\.(get|post|put|patch|delete)\s*\(" src/ --type ts
# Find guard/middleware usage (NestJS guards, Express middleware)
rg -n "@UseGuards\|@Public\|skipAuth\|allowAll\|isPublic" src/ --type ts
# Find raw DB lookups that may lack owner filter
rg -n "findById\|findOne\|findUnique\|findFirst" src/ --type ts | grep -v "owner\|tenant\|userId\|org_id"
# Detect unguarded role or admin fields in update payloads
rg -n "role\s*:\|isAdmin\s*:\|permissions\s*:" src/ --type ts | grep -v "//\|check\|assert\|verify"
# Check for soft-delete bypass: direct ID lookup that does not filter deletedAt
rg -n "where.*id.*=\|findById\|findUnique" src/ --type ts | grep -v "deletedAt\|deleted_at\|isDeleted"
# Map middleware order in Express-style chains (look for route definition after middleware)
rg -n "app\.use\|router\.use" src/ --type ts
# Find cross-tenant admin bypass patterns
rg -n "bypassTenant\|skipTenant\|adminOverride\|superAdmin" src/ --type ts
# Find denied-path assertions in tests
rg -n "403\|Forbidden\|UNAUTHORIZED\|should.*deny\|should.*reject" src/ --include="*.test.*" --include="*.spec.*"
# GraphQL resolver guard check
rg -n "@Resolver\|@Query\|@Mutation" src/ --type ts | head -30
rg -n "context\.user\|ctx\.user\|info\.context" src/ --type ts
# Check for IDOR patterns: params.id used in a query without owner join
rg -nP "params\.(id|userId|orgId)\b" src/ --type ts > /tmp/param_ids.txt
rg -nP "where.*=.*params\." src/ --type ts >> /tmp/param_ids.txt
# then manually review /tmp/param_ids.txt for missing owner conditions
# Prisma/TypeORM scope check: does every model query include tenant_id?
rg -n "prisma\.\w+\.(find|update|delete)" src/ --type ts | grep -v "tenant_id\|tenantId\|orgId\|org_id\|ownerId\|owner_id"
# Check HTTP 403 vs 404 in error handlers
rg -n "res\.status(404)\|res\.status(200)" src/ --type ts | grep -i "not.*found\|forbidden"
# Find JWT decode without verify (authz bypass risk)
rg -n "jwt\.decode\b" src/ --type ts
Severity rubric
Severity
Example
Critical
IDOR confirmed: any user can read/modify another user's record with a changed ID. Role self-promotion: PUT /profile accepts role: "admin". Default-allow catch-all returns 200 for unauthenticated requests.
High
Missing ownership check on one HTTP verb while others are protected. Admin route gated only by path prefix (/admin/) with no middleware role check. Cross-tenant read possible with a guessed org_id.
Medium
404 returned instead of 403, leaking resource existence. Soft-deleted record accessible via direct ID lookup. Access log missing for privileged admin action.
Low
Denied-path tests absent for a resource that has correct guards. ABAC attribute read from request body instead of session (not currently exploitable but fragile). Verbose error message includes internal role name.
Common issues & anti-patterns
Middleware order inversion: auth guard registered after route handler in Express/Koa, making it inert. In Express app.use(guard) must appear beforeapp.use(router) in file order.
Shared ORM scope missing: a base query scope adds tenant_id filter in findAll but not in findOne, update, or delete methods.
Trusting the frontend: permission check exists only in UI code; the API endpoint itself is unprotected. The server must enforce authorization regardless of whether the UI renders the action.
Wildcard CORS + credentials: open CORS with credentials: true lets any origin make authenticated requests using the victim's session cookie — an authorization bypass via cross-site.
Role stored in JWT without server re-validation: if the role claim is read from the JWT payload without checking a DB source of truth, a user who was demoted can keep using an old token.
Soft-delete bypass: records excluded from normal list queries via deletedAt IS NULL are accessible via findById(id) if the soft-delete filter is not applied to direct lookups.
Missing verb coverage: GET /resource/:id has ownership check but PUT /resource/:id does not — the attacker simply changes the HTTP method.
Implicit admin by convention: routes under /admin/ rely solely on path prefix, not on a role assertion in middleware. A path traversal or a misconfigured proxy can bypass the prefix.
ABAC policy reads request body fields: if (req.body.role === 'admin') allowAdminAction() — the user controls the attribute that grants them access.
Required output
Return a structured findings list:
## Authorization Review Findings
### Critical
- [FILE:LINE] Description of issue, attack vector, recommended fix.
### High
- ...
### Medium
- ...
### Low / Informational
- ...
### Verified OK
- List of patterns checked and confirmed safe.
### Coverage gaps
- Entry points without test coverage for denied paths.
### Summary
- Permission model identified: RBAC / ABAC / ad-hoc
- Total routes/resolvers inspected: N
- Total findings: N (critical: N, high: N, medium: N, low: N)
- Single highest-priority fix: [FILE:LINE] one sentence.
Safety
Read files and grep patterns only. Do not modify auth middleware, role tables, or policy files without explicit user instruction.
Do not print, log, or reproduce actual user IDs, tenant IDs, or role values found in config or database seed files.
Do not run live HTTP requests against any environment.
Do not alter test files to make denied-path assertions pass — surface the gap as a finding instead.
1---2name: authz-permission-review3description: Use when you need to review authorization, roles, row-level access, tenancy, privilege checks, and denied paths.4---56# Authz Permission Review78## Purpose910Audit authorization logic across the codebase: RBAC/ABAC role definitions, row-level security, multi-tenant data isolation, IDOR exposure, privilege escalation paths, and the presence of a default-deny posture. Every finding gets a severity, a `file:line` reference, and a concrete remediation step. The review is static and code-driven — no live requests, no live credentials.1112A surface-level "auth check is present" observation is not a finding. The goal is to confirm that every entry point, every HTTP verb, and every DB query is bounded by the authenticated user's identity — and that a default-deny fallback protects anything that is not explicitly covered.1314## When to use1516- A PR adds or modifies middleware, guards, decorators, or policy files that control access.17- A new resource type (API route, DB table, file store) is introduced without visible permission checks.18- A security review or audit explicitly calls for authorization analysis.19- You spot a `req.user` / `session.userId` reference used as a filter but not validated against the resource owner.20- Multi-tenant features are being added or refactored.21- Row-level security (RLS) policies are being added, removed, or bypassed in a migration.2223## When not to use2425- The task is purely about authentication (login, token issuance, MFA) — use a dedicated authn review.26- The codebase has no server-side logic (static site, CDN-only delivery).27- You need penetration testing with live requests — this skill is static/code analysis only.28- The narrower `47-security-and-secrets-review` already covers secrets and CORS; this skill focuses on authorization logic.2930## Procedure31321. **Map the permission model.** Read role/permission definitions: look for `roles`, `permissions`, `policies`, `guards`, `casl`, `casbin`, `oso`, or hand-rolled arrays. Note whether it is RBAC (role-based), ABAC (attribute-based), or ad-hoc. Record where the source of truth lives — a DB table, a JWT claim, a policy file, or a config object.33342. **Enumerate entry points.** List every HTTP route, GraphQL resolver, RPC handler, WebSocket event, and background job that touches user data. Use `rg` to find route registrations, then cross-reference against middleware chains to confirm each entry point has an auth check *before* the handler body runs.35363. **Check row-level isolation.** For every DB query that returns potentially sensitive records, verify the WHERE clause includes `tenant_id`, `org_id`, or `owner_id` tied to the authenticated user — not just passed in from the request body. In ORMs like Prisma/TypeORM, check that base scopes or default scopes include the tenant filter everywhere, not just in the obvious list endpoints.37384. **Hunt for IDOR.** Search for patterns where an ID from the request (path param, query string, body) is used directly in a lookup without an ownership assertion. `findById(req.params.id)` without `AND owner_id = req.user.id` is a textbook IDOR. Check every `findOne`, `findById`, `getOne`, and `select ... where id = $1` call.39405. **Verify default-deny.** Confirm that a route/resource without an explicit allow rule is blocked, not allowed. Check the fallback/catch-all handler. Look for `allowAll()`, `skipAuth()`, `@Public()`, or `isPublic: true` annotations and confirm they are intentional, minimal, and documented with a comment stating why the route is public.41426. **Inspect privilege escalation paths.** Look for self-service role assignment endpoints: can a regular user set `role: "admin"` in an update payload? Are role fields stripped before upsert? Check `PATCH /users/:id` and `PUT /profile` for unguarded role, permissions, or isAdmin fields.43447. **Audit cross-tenant operations.** In multi-tenant apps, check admin/support routes that bypass tenant filters. Confirm they require an explicit elevated role checked in server-side middleware (not just in the UI), and that access is logged with actor, target tenant, and timestamp.45468. **Verify denied path responses.** Confirm that unauthorized access returns 403 Forbidden (not 404 leaking resource existence, not 200 with empty data). Check that error messages do not echo tenant IDs, user IDs, or internal role names back to the caller.47489. **Check HTTP verb coverage.** A route that guards `GET /resource/:id` with ownership check does not automatically guard `PUT /resource/:id` or `DELETE /resource/:id`. Verify all verbs are covered independently.495010. **Check test coverage for denied paths.** Look for tests that assert denied behavior: a user accessing another user's resource should return 403, not 200 or 404. Flag any sensitive resource that has no test exercising the denied path.5152## Concrete checks5354- [ ] Every route has auth middleware registered *before* the handler, not after (check middleware order explicitly in Express/Koa/Fastify chains).55- [ ] Row-level queries always include an owner/tenant filter derived from the verified session, not from the request payload.56- [ ] No IDOR: resource IDs from the request are cross-checked against the authenticated user's identity at the query layer, not just in the handler return.57- [ ] Role assignment requires elevated privilege; regular users cannot self-promote via an unguarded field in an update body.58- [ ] Default posture is deny; allow-lists are explicit, narrow, and annotated with a reason.59- [ ] Cross-tenant admin endpoints are gated on a server-side role check and emit an access log entry.60- [ ] Unauthorized access returns HTTP 403 with no resource detail in the response body.61- [ ] All HTTP verbs on a resource (GET, POST, PUT, PATCH, DELETE) are independently guarded — not just the read path.62- [ ] Unit/integration tests cover at least one denied path per sensitive resource.63- [ ] ABAC policies reference immutable server-side attributes, not user-supplied request fields.64- [ ] Soft-deleted records are excluded from ownership-bypassed direct lookups (not just from normal list queries).65- [ ] JWT role claims are re-validated server-side on each request; a changed role in the JWT does not grant elevated access until re-verified.6667## Commands6869```bash70# Find route registrations (Express/Koa style)71rg -n "router\.(get|post|put|patch|delete)\s*\(" src/ --type ts7273# Find guard/middleware usage (NestJS guards, Express middleware)74rg -n "@UseGuards\|@Public\|skipAuth\|allowAll\|isPublic" src/ --type ts7576# Find raw DB lookups that may lack owner filter77rg -n "findById\|findOne\|findUnique\|findFirst" src/ --type ts | grep -v "owner\|tenant\|userId\|org_id"7879# Detect unguarded role or admin fields in update payloads80rg -n "role\s*:\|isAdmin\s*:\|permissions\s*:" src/ --type ts | grep -v "//\|check\|assert\|verify"8182# Check for soft-delete bypass: direct ID lookup that does not filter deletedAt83rg -n "where.*id.*=\|findById\|findUnique" src/ --type ts | grep -v "deletedAt\|deleted_at\|isDeleted"8485# Map middleware order in Express-style chains (look for route definition after middleware)86rg -n "app\.use\|router\.use" src/ --type ts8788# Find cross-tenant admin bypass patterns89rg -n "bypassTenant\|skipTenant\|adminOverride\|superAdmin" src/ --type ts9091# Find denied-path assertions in tests92rg -n "403\|Forbidden\|UNAUTHORIZED\|should.*deny\|should.*reject" src/ --include="*.test.*" --include="*.spec.*"9394# GraphQL resolver guard check95rg -n "@Resolver\|@Query\|@Mutation" src/ --type ts | head -3096rg -n "context\.user\|ctx\.user\|info\.context" src/ --type ts97```9899```bash100# Check for IDOR patterns: params.id used in a query without owner join101rg -nP "params\.(id|userId|orgId)\b" src/ --type ts > /tmp/param_ids.txt102rg -nP "where.*=.*params\." src/ --type ts >> /tmp/param_ids.txt103# then manually review /tmp/param_ids.txt for missing owner conditions104105# Prisma/TypeORM scope check: does every model query include tenant_id?106rg -n "prisma\.\w+\.(find|update|delete)" src/ --type ts | grep -v "tenant_id\|tenantId\|orgId\|org_id\|ownerId\|owner_id"107108# Check HTTP 403 vs 404 in error handlers109rg -n "res\.status(404)\|res\.status(200)" src/ --type ts | grep -i "not.*found\|forbidden"110111# Find JWT decode without verify (authz bypass risk)112rg -n "jwt\.decode\b" src/ --type ts113```114115## Severity rubric116117| Severity | Example |118|----------|---------|119| **Critical** | IDOR confirmed: any user can read/modify another user's record with a changed ID. Role self-promotion: `PUT /profile` accepts `role: "admin"`. Default-allow catch-all returns 200 for unauthenticated requests. |120| **High** | Missing ownership check on one HTTP verb while others are protected. Admin route gated only by path prefix (`/admin/`) with no middleware role check. Cross-tenant read possible with a guessed `org_id`. |121| **Medium** | 404 returned instead of 403, leaking resource existence. Soft-deleted record accessible via direct ID lookup. Access log missing for privileged admin action. |122| **Low** | Denied-path tests absent for a resource that has correct guards. ABAC attribute read from request body instead of session (not currently exploitable but fragile). Verbose error message includes internal role name. |123124## Common issues & anti-patterns125126- **Middleware order inversion**: auth guard registered after route handler in Express/Koa, making it inert. In Express `app.use(guard)` must appear *before* `app.use(router)` in file order.127- **Shared ORM scope missing**: a base query scope adds `tenant_id` filter in `findAll` but not in `findOne`, `update`, or `delete` methods.128- **Trusting the frontend**: permission check exists only in UI code; the API endpoint itself is unprotected. The server must enforce authorization regardless of whether the UI renders the action.129- **Wildcard CORS + credentials**: open CORS with `credentials: true` lets any origin make authenticated requests using the victim's session cookie — an authorization bypass via cross-site.130- **Role stored in JWT without server re-validation**: if the role claim is read from the JWT payload without checking a DB source of truth, a user who was demoted can keep using an old token.131- **Soft-delete bypass**: records excluded from normal list queries via `deletedAt IS NULL` are accessible via `findById(id)` if the soft-delete filter is not applied to direct lookups.132- **Missing verb coverage**: `GET /resource/:id` has ownership check but `PUT /resource/:id` does not — the attacker simply changes the HTTP method.133- **Implicit admin by convention**: routes under `/admin/` rely solely on path prefix, not on a role assertion in middleware. A path traversal or a misconfigured proxy can bypass the prefix.134- **ABAC policy reads request body fields**: `if (req.body.role === 'admin') allowAdminAction()` — the user controls the attribute that grants them access.135136## Required output137138Return a structured findings list:139140```141## Authorization Review Findings142143### Critical144- [FILE:LINE] Description of issue, attack vector, recommended fix.145146### High147- ...148149### Medium150- ...151152### Low / Informational153- ...154155### Verified OK156- List of patterns checked and confirmed safe.157158### Coverage gaps159- Entry points without test coverage for denied paths.160161### Summary162- Permission model identified: RBAC / ABAC / ad-hoc163- Total routes/resolvers inspected: N164- Total findings: N (critical: N, high: N, medium: N, low: N)165- Single highest-priority fix: [FILE:LINE] one sentence.166```167168## Safety169170- Read files and grep patterns only. Do not modify auth middleware, role tables, or policy files without explicit user instruction.171- Do not print, log, or reproduce actual user IDs, tenant IDs, or role values found in config or database seed files.172- Do not run live HTTP requests against any environment.173- Do not alter test files to make denied-path assertions pass — surface the gap as a finding instead.
Run npx skillmds@latest add fluxonlab/authz-permission-review in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Use when you need to review authorization, roles, row-level access, tenancy, privilege checks, and denied paths. It is listed under Security on SkillMD.
This skill has not completed SkillMD's automated safety review yet. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
FluxonLab (@fluxonlab) published this skill. Their other Agent Skills are listed on their SkillMD profile.