After enough assessments the first hour stops being exploratory. There are five things I check immediately, because they are present in some form in most codebases, and any one of them can be the whole report.
1. Broken object level authorisation
The endpoint checks that you are logged in. It does not check that the record belongs to you. Change the ID, get someone else's data. It remains the most common critical finding in API testing and the easiest to prove: two accounts, one request, swapped identifier.
The fix is structural, not per-endpoint. Scope every query to the authenticated principal at the data-access layer — a base query that always filters by owner or tenant — so a developer must actively opt out rather than remember to opt in. Then add a test that asserts a cross-account request returns 404, and make it part of the template for new resources.
2. Mass assignment
A profile update endpoint accepts the whole request body and passes it to the model. The body contains role, is_verified, credit_balance or account_id. The client never sends those fields, so nobody noticed they were accepted.
- Bind requests to explicit input objects listing the fields you accept.
- Never pass raw request data into an ORM create or update call.
- Assert in tests that privileged fields are ignored when submitted.
3. Serialisers that over-fetch
The user list endpoint powers an avatar row, so nobody looked at the response — which contains every user's email address, phone number and internal identifier. The interface renders one field; the API returns forty.
Define response shapes per endpoint rather than serialising models directly. It is more code and it is the only approach that stays correct when someone adds a column.
4. Missing rate limits where it counts
- Login, OTP request, OTP verification, password reset and email change — all need per-account and per-IP limits.
- OTP verification without a limit is a four-digit code brute-forced in seconds.
- Expensive search and export endpoints need limits too, or they become your denial-of-service surface.
- Enforce at the gateway, so a new service cannot forget.
5. Endpoints nobody remembers
The v1 API left routable "for the old app version". A debug endpoint added during an incident. An internal admin service exposed through the same load balancer. These are never in the documentation, and they are frequently the way in — old code does not get the authorisation improvements the current code received.
- Maintain an endpoint inventory generated from routing, not written by hand.
- Version deprecation needs a removal date in a ticket, not a note in a wiki.
- Internal services belong on a separate network path, not on a path prefix.
The pattern behind all five
Each one is a case of the server trusting something it should verify, or exposing something nobody looked at because the client did not render it. Fix them at the framework level — base query scoping, explicit input and output shapes, gateway limits and a generated route inventory — and they stop recurring in every new feature.