Broken access control has topped the OWASP Top 10 for years, and the reason is structural: it's easy to correctly implement authentication (is this a real user?) while forgetting authorization (is this user allowed to do this specific thing?).
The core pattern: IDOR
Insecure Direct Object Reference is the most common manifestation. An endpoint like /api/orders/12345 correctly checks the requester is logged in, but never checks whether the logged-in user actually owns order 12345. Change the number, get someone else's order.
GET /api/orders/12345
# Checks: is there a valid session? ✓
# Missing: does this session's user own order 12345?
Why this keeps happening
Authentication is usually handled centrally — a middleware or decorator applied consistently. Authorization is often scattered across individual endpoints, decided case-by-case by whoever wrote that specific route. This makes it easy for one endpoint to correctly implement it while a similar endpoint, written by someone else or added later, misses it entirely.
Fixing it architecturally, not endpoint-by-endpoint
Relying on every developer remembering to add an authorization check to every new endpoint doesn't scale. Stronger patterns:
- Centralize authorization logic in a shared layer or policy engine, rather than reimplementing checks inline in each handler.
- Default deny. Design the framework so access requires an explicit grant, rather than requiring an explicit denial to block something.
- Scope database queries to the requesting user by default, where the ORM or query layer makes it structurally difficult to accidentally fetch another user's data.
Horizontal vs. vertical access control
- Horizontal — a regular user accessing another regular user's data (the IDOR pattern above).
- Vertical — a regular user accessing admin-level functionality they shouldn't have.
Both need explicit checks; a system can correctly handle one and still miss the other.
Testing for it
Automated scanners struggle to reliably catch broken access control, since it requires understanding what a specific user should and shouldn't access — which is business logic, not a generic pattern. Manual testing (attempting to access another account's resources with a valid session) and thorough code review remain the most reliable detection methods.
Fixing broken access control isn't about writing more careful code on each endpoint — it's about making the insecure path structurally harder to write than the secure one.