Broken Object-Level Authorization: The Bug That Needs a Second Test Account
Most of the serious findings we report are not exotic. They are a valid user, sending a valid request, for an object that is not theirs — and the server handing it over. OWASP ranks this first in its API Security Top 10, and it is still the single most common high-severity issue we find on API assessments.
TL;DR — Tooling can tell you a field is missing a length check. Deciding whether invoice 4471 should be visible to this user is a question about your business, not your code. Automation can be configured to compare two identities and flag the difference; what it cannot do by default is know which answer was correct.
What it looks like in practice
The pattern is almost always the same. An endpoint takes an identifier:
GET /api/v2/invoices/4471
The application checks that you are logged in. It does not check that invoice 4471 belongs to you. Change the number, get someone else's invoice. The request is perfectly well-formed, the response is a clean 200, and nothing in the logs looks unusual — which is exactly why it survives.
It shows up in the same places over and over: sequential database IDs in URLs, an object identifier moved into a JSON body where reviewers stop looking, a mobile app that trusts a customer ID it received from the client, and multi-tenant systems where the tenant is derived from the payload rather than the session.
Why default scanning misses it
An unauthenticated or single-session scan has no model of ownership. It sees that /invoices/4471 returned 200 and has no basis for judging whether that was correct. There is no generic signature for “this record belonged to someone else”.
This is not a claim that tooling can never help. Modern API and DAST tooling can be configured with two authenticated identities and set to replay requests across them, and a well-tuned setup will surface candidate authorization gaps. That configuration is the work: someone has to define the identities, map which objects each one legitimately owns, and decide which of the differences actually matter.
So the honest dividing line is not tool versus human. It is business context. Automation is good at generating the comparison; establishing intended ownership and validating real impact is what a human-led assessment adds.
Four checks worth running this week
- Two accounts, same tier. Create two ordinary customer accounts. Take any request from account A and replay it with account B's session. Anything that returns data instead of a 403 is a finding. This one exercise catches most of what we report.
- Look past the URL. Object identifiers hide in JSON bodies, headers, and nested arrays for bulk operations. Bulk endpoints are worth particular attention — per-item authorization is often skipped when a request handles fifty records at once.
- Check writes, not just reads. Teams that have fixed
GETfrequently still allowPATCHorDELETEon objects the user does not own. The read path gets reviewed because it is the obvious one. - Enforce it in one place. If every controller re-implements its own ownership check, one of them is wrong. Centralise the decision so that fetching an object and authorising it cannot be done separately.
On UUIDs
Replacing sequential IDs with UUIDs is often proposed as the fix. It is a reasonable hardening step — it makes blind enumeration impractical — but it is not an authorization control. Identifiers leak through shared links, exports, referrer headers, support tickets and previous responses. If the only thing protecting a record is that its ID is hard to guess, the record is not protected. Fix the authorization check; treat the UUID as defence in depth.
Where it fits in an assessment
This is the work we do by hand on every API engagement: map the objects, establish who owns what, then systematically try to cross those boundaries with approved test accounts and seeded records. We retrieve only what is needed to demonstrate the gap, and the report shows both what we could reach and the boundaries that held.
If you want this run against your own APIs, get in touch. Two test accounts and a short scoping call are usually all we need to start.
