$npx -y skills add briiirussell/cybersecurity-skills --skill api-auditAudit REST, GraphQL, and RPC APIs against the OWASP API Security Top 10 (2023). Use when the user mentions 'API security,' 'API audit,' 'BOLA,' 'broken object level authorization,' 'BFLA,' 'function-level authorization,' 'mass assignment,' 'API rate limiting,' 'GraphQL security,'
| 1 | # API Audit — REST / GraphQL / RPC Security Review |
| 2 | |
| 3 | Perform a systematic security audit of API endpoints against the OWASP API Security Top 10 (2023). Distinct from `owasp-audit` — that's category-driven over a whole codebase, this is surface-driven over the API contract. |
| 4 | |
| 5 | Use `owasp-audit` for the codebase as a whole. Use this when you need a focused pass over every endpoint with API-specific bypass patterns. They cross-reference each other where categories overlap. |
| 6 | |
| 7 | ## Scope the Audit |
| 8 | |
| 9 | 1. Inventory every API surface — REST routes, GraphQL resolvers, tRPC procedures, gRPC services, Server Actions, webhook handlers, internal RPC |
| 10 | 2. Identify auth model — JWT, session cookies, API keys, mTLS, OAuth scopes |
| 11 | 3. Identify the tenancy model — single-tenant, multi-tenant, row-level isolation |
| 12 | 4. Map sensitive resources — user data, payments, files, admin functions |
| 13 | |
| 14 | ## Audit Checklist |
| 15 | |
| 16 | ### API1: Broken Object Level Authorization (BOLA) |
| 17 | |
| 18 | The #1 API vulnerability by exploitation frequency. Every endpoint that accepts an object ID needs an explicit ownership check before reading or mutating. |
| 19 | |
| 20 | - For each route that takes an ID parameter (`/users/:id`, `/orders/:id`, `/projects/:id`), verify a query like `findFirst({ where: { id, userId } })` runs before any data access — not `findById(id)` then a separate check |
| 21 | - ORM relation traversal: `posts.find(id).user.creditCard` returns another tenant's card if the relation isn't guarded. Audit every `.include`, `.with`, `includes`, eager-loaded relation |
| 22 | - UUIDs are not access control. Sequential IDs make enumeration trivial; UUIDs only slow it down |
| 23 | - Predictable surrogate keys (slugs, public_ids) used as if they were unguessable |
| 24 | - Grep for: `params.id`, `req.params.<id>`, `formData.get("<id>")`, ORM `.findById(` / `.find(` without a `where` clause, GraphQL resolvers that take an `id` arg and call `.findUnique` |
| 25 | |
| 26 | ### API2: Broken Authentication |
| 27 | |
| 28 | - JWT with `alg: none` accepted by the verifier |
| 29 | - JWT secret comparison without `crypto.timingSafeEqual` / equivalent |
| 30 | - Refresh tokens that never rotate or have no revocation list |
| 31 | - Password reset that returns a token in the response body instead of mailing it |
| 32 | - API keys passed in URL query string (logged everywhere — access logs, CDN, proxies) |
| 33 | - Bearer-token compare against `process.env.X` without a presence check — when X is unset, `"Bearer ${undefined}"` is a valid literal |
| 34 | - Grep for: `jwt.verify`, `jsonwebtoken`, `Bearer ${process.env`, `jwt.decode` (without verify), `verify.*alg` |
| 35 | |
| 36 | ### API3: Broken Object Property Level Authorization (BOPLA / BFLA / Excessive Data Exposure) |
| 37 | |
| 38 | - API returns the whole DB row instead of a curated DTO — `res.json(user)` leaks `password_hash`, `stripe_customer_id`, internal flags |
| 39 | - Admin-only fields (role, is_verified, tenant_id) accepted on update endpoints from regular users — **mass assignment** |
| 40 | - GraphQL exposes admin-mutation fields without role-based field-level auth — `mutation UpdateUser($id, $role)` succeeds because the resolver only checks "is the user logged in" |
| 41 | - Grep for: `res.json(<entity>)` without explicit field projection, `Object.assign(record, req.body)`, Mongoose `findByIdAndUpdate(id, req.body)`, Drizzle `.update().set(req.body)`, Sequelize `update(req.body)`, GraphQL resolvers without role checks |
| 42 | |
| 43 | ### API4: Unrestricted Resource Consumption |
| 44 | |
| 45 | - No rate limit on auth endpoints (login, signup, password-reset, SMS-send, email-verify) |
| 46 | - No per-tenant quota on expensive operations (LLM calls, search, file processing, webhook fan-out) |
| 47 | - Page size unbounded — `?limit=10000000` returns 10M rows |
| 48 | - GraphQL query depth not capped — `user { posts { user { posts { ... } } } }` runs forever |
| 49 | - GraphQL query complexity not analyzed — single query that triggers N+1 against 100M rows |
| 50 | - Webhook handlers that re-trigger expensive work without idempotency |
| 51 | |
| 52 | ### API5: Broken Function Level Authorization (BFLA) |
| 53 | |
| 54 | - Auth check on the route but not on the handler — wildcard middleware misses a manually-mounted route |
| 55 | - "Admin-ish" endpoints reachable by changing `POST /api/v1/users/me` to `POST /api/v1/users/<other_id>` |
| 56 | - HTTP verb tampering — `DELETE /admin/users/123` blocked, but `POST /admin/users/123/delete` succeeds |
| 57 | - Conditional auth based on `req.user.role === "admin"` where role is set from a header the client controls |
| 58 | - Sister-route gaps — `PUT /:id` is guarded but `POST /:id/send` writes the same row without the guard. Run sister-route audit (see `owasp-audit`) |
| 59 | - Grep for: every route handler — does it cal |