The endpoint your UI never shows
BOLA (API1) is "may you read this record". BFLA (API5) is "may you call this function" — and it is the easier one to miss, because it usually lives at an endpoint the UI never calls.
The typical mechanism, and it recurs in every product:
- There is a
DELETE /api/users/{id}for the admin screen. - The UI only shows the Delete button to admins. Product-wise, that is the job done.
- Nobody tests that endpoint, because "nobody can reach it".
- Production Swagger lists it, schema included.
The point worth saying plainly: a UI is not a security control. It is a hint about intent. Hiding a button does not stop a request; it only stops a user sending that request by accident.
Three common variants, ordered by how often I find them in real code:
| Variant | What it looks like | Why it passes review |
|---|---|---|
| The "internal" endpoint | /api/internal/reindex, no attribute at all | The word "internal" in a path feels like protection |
| A new method on an old controller | Class carries [Authorize(Roles="Admin")], a new action adds [AllowAnonymous] for testing and it never comes off | The diff is one line, and it is the last line |
| The HTTP method nobody thought about | GET /api/reports/{id} is authorised, DELETE on the same route is not | Reviews read by route, not by route × method pair |
Why counting by eye is not enough: a mid-sized API has 200–400 route × method pairs. A human reading them all once misses some, and re-reading them every sprint is something nobody does. The only durable answer is deny by default — see the fix step.
Comments
Commenting needs an account with at least one completed lesson. That condition is what keeps this thread worth reading: every point belongs to someone who can be asked back, and reputation accrues over time.
You can still read every comment below without an account. Signing in brings you back to this exact spot, not to the top of the page.
Loading comments…