Broken Access Control
In one line: Authorization decides what an authenticated user may do, and it's the most common serious web flaw because the check is easy to forget — the app confirms you're logged in but never that this resource or action is yours — so the fix is to enforce access centrally, server-side, deny-by-default, on every request.
You've proven who you are (that was authentication). Now: are you allowed to do this specific thing? Open this invoice, delete that account, hit the admin page? Broken access control is when the answer should be "no" but the app lets you anyway. It's the single most prevalent category in the OWASP Top 10 (A01) for a simple reason: authentication is a visible feature you build once, but authorization is an invisible check you must remember on every endpoint and every object — and humans forget. The classic case: you view your own order at /orders/501, change it to /orders/502, and you're reading a stranger's order. The app checked you were logged in; it never checked the order was yours. That one missing line is, across the industry, the most common way data leaks.
Why authorization is forgotten so often
Three structural reasons access control is the most-missed control:
- It's per-object and per-action, not once. Authentication is a single gate. Authorization must be re-checked for every resource and every operation — thousands of checks across an app, any one of which can be omitted silently.
- The happy path hides it. In normal use, the UI only ever shows you your own data, so missing checks aren't visible in testing. The bug only appears when someone deliberately requests what the UI never offered — the attacker's mindset.
- Client-side "enforcement" isn't enforcement. Hiding an admin button or omitting a link does nothing; the API endpoint is the real boundary, and an attacker calls it directly.
- Authorization (authz) / access control — enforcing what an authenticated identity is permitted to do.
- IDOR (Insecure Direct Object Reference) — accessing another user's object by changing its identifier (the
/orders/502case); a subtype of broken object-level authorization. - Function-level (or endpoint) access control — checking that a user may invoke a given operation (e.g., only admins can call
DELETE /users). - Privilege escalation — gaining higher rights than granted: vertical (user → admin) or horizontal (one user → another user's data/actions).
- RBAC / ABAC — Role-Based / Attribute-Based Access Control: models for expressing who can do what (by role, or by attributes/policies).
- Deny by default — start from "no access" and grant explicitly; the safe default (an application of least privilege).
The three faces of broken access control
1. Object-level (IDOR) — "can I see your data?"
The endpoint GET /api/invoices/{id} returns an invoice. The code:
invoice = db.invoices.find(id) // fetch by the id from the URL
return invoice // ...and just return it
It authenticated the user (valid session) but never checked ownership. The attacker, logged into their own account, simply iterates: /api/invoices/1001, 1002, 1003… and harvests every customer's invoice. The fix is one clause — scope the query to the caller:
invoice = db.invoices.find(id)
if invoice.owner_id != current_user.id: // the missing check
return 403 Forbidden
return invoice
Better still, make the query itself ownership-scoped (WHERE id = ? AND owner_id = ?) so an un-owned object is never even fetched. Using unguessable IDs (UUIDs) raises the bar but is not a fix — it's security by obscurity; the authorization check is what actually protects the data.
2. Function-level — "can I call that operation?"
An app hides the "Delete user" button for non-admins but the endpoint DELETE /api/users/{id} doesn't verify the caller's role. A regular user calls it directly and deletes accounts. The UI is not a security boundary; every privileged endpoint must check the role server-side.
3. Privilege escalation — "can I become more?"
The user tampers with something that determines their rights: a role=user field in a request body, cookie, or JWT they can edit; a mass-assignment that lets them set isAdmin=true on their own profile update; a workflow step they skip. Vertical escalation (→ admin) or horizontal (→ another user) both stem from trusting client-supplied authority data.
A profile-update endpoint blindly maps the JSON body onto the user record:
user.update(request.body) // whatever fields are in the body get written
The form normally sends {name, email}. The attacker adds a field: {"name":"x","isAdmin":true}. The endpoint writes isAdmin=true to their own account. They're now an admin. The flaw is trusting client input to set authority fields. Fix: allowlist which fields a user may set (never bind role/isAdmin from input), and derive privilege server-side.
How to enforce access control properly
- Deny by default. Every endpoint requires an explicit authorization decision; the absence of a check should mean no access, not open. (Frameworks that "secure by default — every route needs an
@authorize" prevent the "forgot one" failure.) - Centralize the logic. Don't scatter ad-hoc
if user.role == ...checks across hundreds of handlers. Use middleware/policies/a single authorization layer so the rule is defined once and consistently applied — and auditable. - Enforce server-side, at the API. The server is the only real trust boundary. Client-side hiding is UX, not security.
- Check object ownership on every object access, ideally by scoping queries to the current user so un-owned objects can't be returned at all.
- Never trust client-supplied authority. Roles, permissions, prices, user IDs, and
isAdminflags come from the server's record of the session — never from a request field. Allowlist writable fields. - Test for it deliberately. Because the happy path hides these bugs, you must actively try the attacker moves: change IDs, call admin endpoints as a normal user, replay another user's requests. This is core pentest methodology.
Authenticate once; authorize every time. Every request that touches a protected resource or action must independently answer "is this identity allowed to do this to this object?" — server-side, deny-by-default. Most access-control breaches are simply that question going unasked.
Why it matters
- It's #1. Broken Access Control is the top category in the current OWASP Top 10 — the most prevalent and among the most damaging, because it directly exposes data and privileged actions.
- The bugs are simple but invisible. A single missing ownership check leaks an entire customer base. No exotic exploit required — just an iterated ID — which is why automated and manual access-control testing is essential.
- It's pure Foundations applied. Trust boundaries, least privilege, the attacker's mindset, deny-by-default — access control is where all four cash out in code.
Common pitfalls
- Checking authentication but not authorization. "They're logged in" ≠ "they may do this." Add the per-object, per-action check.
- Enforcing only in the UI. Hidden buttons and omitted links stop nothing; attackers call the API directly. Enforce server-side.
- Relying on unguessable IDs (UUIDs) instead of checks. Obscurity raises effort, not security — leaked/enumerated IDs still work. The authorization check is the control.
- Trusting client-supplied roles/IDs/flags. Mass assignment and editable
role/isAdminfields are escalation waiting to happen. Allowlist writable fields; derive authority server-side. - Scattering inconsistent checks. Hand-rolled
ifchecks in every handler guarantee one gets missed. Centralize authorization in middleware/policies and deny by default. - Not testing the unhappy path. If you only test as the intended user, you'll never see the bug. Deliberately try other users' IDs and privileged endpoints.
Page checkpoint
Did access control click?
Pass to unlock the Next button belowWhat's next
→ Continue to Server-Side Request Forgery (SSRF) — back to the injection family, but with a twist: the attacker makes your server perform requests on their behalf, pivoting into internal systems and cloud metadata.
→ Going deeper: access-control models at scale (RBAC/ABAC, policy engines, zero trust) are in Cloud & Identity Security; finding these bugs is core to Penetration Testing.