Broken Access Control and IDOR
Broken access control is the most common serious web flaw: the app authenticates you fine, then fails to check whether this user may access this resource or action. The signature case is IDOR — changing an ID in a URL, /invoice/1001 to /invoice/1002, and seeing someone else's data because the server never checked ownership.
This topic shows why authorization must be enforced server-side on every object and action, and how a missing check hands the intruder other customers' data on Meridian. It is the number-one category in the OWASP Top 10 for a reason: it is trivially easy to omit.
Authentication Is Not Authorization
Being logged in is not permission to touch a specific record. Broken access control is forgetting the second check — the app confirms who you are, then acts on the object you named without confirming you may. That gap is so common because authorization is scattered across endpoints and easy to leave off just one of them.
IDOR — Insecure Direct Object References
The app exposes a database key and trusts the client's copy of it: incrementing or guessing the ID reaches objects you do not own, because ownership was never verified server-side. Unguessable IDs like UUIDs slow enumeration but are not authorization — obscurity is not a control, and the ownership check must still exist on every access.
# VULNERABLE: returns any invoice by id, ownership never checked
@app.get('/api/invoice/<int:id>')
def invoice(id):
return db.invoices.get(id) # attacker just increments id
# FIXED: scope the lookup to the authenticated user's own records
@app.get('/api/invoice/<int:id>')
def invoice(id):
inv = db.invoices.get(id)
if inv is None or inv.owner_id != current_user.id:
abort(404) # not yours -> as if it does not exist
return inv
The vulnerable handler returns whatever invoice ID the client asks for, so the intruder harvests every customer's invoices by counting upward. The fix verifies that the requesting user owns the record on every access, and returns a 404 when they do not — revealing nothing about whether the record exists. The ownership check, done server-side on every object, is the entire defense.
Function- and Field-Level Access Control
Beyond objects, access control must cover actions and fields. Hidden admin endpoints reachable by guessing a URL, and API responses that return fields the user should not see, are the same flaw at a different granularity. "We did not link to it" is not access control; the endpoint is still reachable by direct request, and the extra field is still in the JSON.
Why It's So Common, and the Fix
Authorization scattered per-endpoint means one is inevitably forgotten. The durable fix is centralizing checks and denying by default (Chapter 3), so a new endpoint is locked until explicitly opened rather than open until someone remembers to lock it. Access control is not a feature you add to sensitive endpoints; it is a default every endpoint inherits.
- Checking that the user is logged in but not that they own or may access the specific object — the essence of IDOR.
- Relying on unguessable IDs (UUIDs) as the only control — obscurity slows enumeration but is not authorization; the ownership check must still exist.
- Hiding admin functions by not linking to them (security by obscurity) while the endpoints remain reachable by direct request.
- Scattering authorization logic per-endpoint so one is inevitably forgotten, instead of enforcing it centrally and deny-by-default.
- Returning fields in an API response that the user is not authorized to see.
- Enforce authorization on every request against the specific object and action, server-side, deny-by-default (Chapter 3).
- Verify ownership or permission on the record for every access rather than trusting client-supplied IDs; treat UUIDs as defense in depth, not the control.
- Centralize access-control decisions in a policy layer so coverage is systematic and new endpoints are locked by default.
- Enforce access control at the field level, not just the object level, so responses reveal only what the user may see.
- Test explicitly for IDOR and missing function-level checks, including trying other users' object IDs.
Knowledge Check
What is the core mistake behind an IDOR vulnerability?
- The server trusts a client's ID without an ownership check
- The object IDs are encrypted too weakly
- The user was never authenticated at all
- The database uses sequential integer keys instead of random ones
Why are unguessable UUIDs not a substitute for an authorization check?
- Obscurity slows guessing; a leaked ID still grants access
- UUIDs are actually easier to guess than sequential integers
- UUIDs cannot be used as database keys
- UUIDs automatically enforce ownership
Why is deny-by-default with centralized checks the durable fix for broken access control?
- A new endpoint stays locked until it is authorized
- It makes every endpoint public unless traffic is high
- It removes the need to authenticate users
- It encrypts every object ID automatically
You got correct