A checklist is only useful if every line is something you can actually check today. This one is ordered by what breaks in practice — authorisation first, transport last — and each item says how to verify it, not just what to want. Work down it; stop when you run out of afternoon and pick up where you left off.
1. Authorization — where the real findings are
- Every id-scoped route filters by owner in the same query. Verify by reading one route per resource type, not by trusting the middleware. Two-step lookups — find-then-check — drift apart in refactors.
- A second account gets a 404, not a 403. Verify live: register two accounts in separate tenants and request the first account's id as the second. A 403 tells the caller the object exists. Background: IDOR / BOLA.
- Unauthenticated requests to id-scoped routes return 401/404. Verify with the same request and no session header at all — this is the variant that ends up in public search indexes.
- Privileged paths reject a normal user. Verify by requesting
/admin-shaped routes —/api/admin/*,/internal/*,/api/export,/api/users— with a low-privilege session. A 200 is broken function-level authorization. - Responses are serialised from an explicit field list. Verify by reading one serialiser: if it returns the model object, a new column ships to clients the day it is added.
- Routes are deny-by-default. Verify by adding a throwaway endpoint with no decorator and confirming it is unreachable rather than public.
- There is a cross-account test in CI per route shape. Verify by deleting the ownership filter locally and confirming a test goes red. If none does, the control is a convention, not a control.
# The three requests that decide item 2, 3 and 4 — run them against a real deploy
curl -H "$A_SESSION" https://api.example.com/invoices/8842 # 200, note an email/account no.
curl -H "$B_SESSION" https://api.example.com/invoices/8842 # want 404 — a 200 with A's data = BOLA
curl https://api.example.com/invoices/8842 # want 401/404 — a 200 = public object
curl -H "$B_SESSION" https://api.example.com/admin/users # want 403 — a 200 = BFLA2. Authentication and sessions
- Tokens expire — verify by replaying yesterday's token; an accepted one means no expiry is enforced.
- Logout invalidates the refresh token — verify by logging out, then using the refresh token to mint a new access token. It should fail.
- JWT signatures are verified and
algis pinned — verify by sending a token re-signed withalg: noneor a different algorithm; it must be rejected before any claim is read. - The login endpoint is rate limited — verify by sending 20 wrong passwords and watching for a 429. See login rate limiting.
- Password reset and email-change tokens are single-use and short-lived — verify by using one twice.
- Session cookies carry
Secure,HttpOnlyand a saneSameSite— verify with the cookie checker.
3. Input and resource limits
- Every query goes through parameterised statements — verify by grepping for string-built SQL; there should be zero hits from request input.
- Pagination has a server-side maximum — verify by requesting
?limit=100000and checking what actually comes back. - GraphQL has depth and complexity limits and introspection is off in production — verify by sending an introspection query to the live endpoint.
- Any parameter that becomes an outbound URL is allow-listed — verify by pointing it at a private address and a cloud metadata address; both must be refused before the request is made.
- Uploads are limited by size and type, and stored off the web root — verify by uploading an oversized file and a file whose extension does not match its content.
- Errors do not return stack traces or SQL text — verify by forcing a 500 on a staging clone and reading the body.
4. Surface and transport
- You have a current list of every public hostname. Verify against certificate transparency logs, not against memory — the subdomain finder does this in one request.
- No staging, dev or internal API answers publicly — verify each host from that list; this is the item that finds the most.
- The OpenAPI document and any API console are intentionally public or not reachable — verify by requesting
/openapi.json,/swagger,/graphqlunauthenticated. - HTTPS only, with HSTS, and HTTP redirects before anything is read — verify with the headers checker.
- CORS does not reflect an arbitrary
Originwith credentials — verify by sending a made-up origin and readingAccess-Control-Allow-Origin. - No secrets in the front-end bundle — verify by grepping the built JS for key-shaped strings; API keys shipped to the browser are public keys.
5. Cadence — what runs when
- Every deploy (CI): the cross-account tests, schema fuzzing against the spec, a grep for string-built SQL.
- Weekly (automated): the public hostname list, transport and header hygiene on each host, exposed spec and console paths.
- Every release (manual, ~1 hour): rebuild the endpoint inventory and re-run the authorisation matrix against endpoints that return customer data.
- Twice a year (a person): business-logic abuse — a refund that accepts a negative amount, a workflow step callable out of order, a role that can read an audit log.
The method behind each of these is in the API security testing guide, and which tool covers which line is in API security testing tools. The list itself maps onto the OWASP API Security Top 10 — this is that list rewritten as things you can check rather than risks you can name.
Section 4 is the part you can check right now, with no credentials: transport, headers, cookies and exposed files on any host. Free, no signup, one request per host.
Check your API host →If you are the owner, not the developer
Hand this list to whoever builds your API and ask for the answers in writing — specifically for section 1, where each item names an observable result. “Yes, we check permissions” is not an answer; “here is the test that fails when we remove the check” is. If the answers are vague on the second-account item, that is the one to get tested independently.
We run section 1 as an engagement: two accounts you create, the authorisation matrix across your real endpoints, and proof in the report rather than a severity label. Quoted per application, from €900, under written authorization.
Ask about a deep audit →