Broken access control examples almost always come down to one failure: the application authenticates who you are but never properly checks what you are allowed to do, so a user reaches data or actions that should be off limits. To define broken access control simply — it is any gap that lets a user act outside their intended permissions. OWASP ranks it as the number-one web application security risk, and the reason is that these bugs are trivial to exploit and easy to introduce.
This guide walks through concrete broken access control examples, explains what each broken access control attack looks like, and gives the fix for each.
Insecure direct object references (IDOR)
The most common broken access control example is the IDOR: an endpoint accepts an identifier from the user and returns the matching record without checking that the record belongs to the requester.
GET /api/invoices/1043 -> returns your invoice
GET /api/invoices/1044 -> returns someone else's invoice
The broken access control attack example here is embarrassingly simple: change the number in the URL. If invoice 1044 belongs to another customer and the server hands it over, you have an IDOR. The same pattern shows up with sequential user IDs, order numbers, and document keys.
The fix is an ownership check on every object lookup — never trust the ID alone:
const invoice = await Invoice.findById(id);
if (!invoice || invoice.ownerId !== req.user.id) {
return res.status(404).end(); // 404, not 403, to avoid leaking existence
}
Returning 404 rather than 403 avoids confirming that the record exists, which denies attackers an enumeration oracle.
Missing function-level authorization
Another frequent broken access control attack targets administrative functions that are hidden in the UI but not protected on the server. The developer removes the "Delete user" button for non-admins and assumes the job is done, but the underlying endpoint still accepts requests from anyone.
POST /api/admin/users/88/delete
If a standard user can call that route directly and it works, the authorization lives only in the front end — which is no authorization at all, because the client is fully attacker-controlled.
The fix is to enforce the role on the server at the route level, ideally through middleware so no handler can be added without a check:
router.post(
"/admin/users/:id/delete",
requireRole("admin"), // server-side gate, always runs
deleteUserHandler
);
Forced browsing to unprotected pages
Forced browsing is guessing or discovering URLs that are not linked anywhere but are not protected either — /admin, /config, /reports/2025-q4.pdf. If sensitive pages rely on being unlinked ("security through obscurity") rather than on an access check, they are one lucky guess or one leaked link away from exposure.
The fix is that every sensitive route requires an authorization decision regardless of whether the UI links to it. Default-deny routing, where a request must explicitly pass a check to succeed, beats default-allow every time.
Privilege escalation through mass assignment
A subtler broken access control example is mass assignment. An endpoint that updates a user profile binds the whole request body onto the model, and the request body includes a field the user should not control:
PATCH /api/users/me
{ "displayName": "Alex", "role": "admin" }
If the framework happily writes role because it accepted every field in the body, the user just promoted themselves. This is a vertical privilege escalation delivered through the data layer rather than a route.
The fix is to allowlist the fields a user may change and ignore everything else:
const allowed = ["displayName", "avatarUrl", "timezone"];
const updates = pick(req.body, allowed); // role can never be set here
await User.update(req.user.id, updates);
Metadata and CORS misconfiguration
Access control also breaks at the edges. A JWT whose signature is not verified, a session cookie without HttpOnly and SameSite, or an overly permissive CORS policy that reflects any Origin all let attackers reach data across a trust boundary. These are not application-logic bugs so much as configuration bugs, but the OWASP category folds them in because the outcome is identical: someone acts outside their intended permissions.
Principles that prevent all of these
Every fix above is an instance of a few durable principles:
- Deny by default. A request should fail unless something explicitly authorizes it. Centralize this so no new endpoint ships without a check.
- Check authorization server-side, every time. Hidden UI elements are convenience, not security.
- Enforce ownership, not just authentication. Being logged in is not permission to touch a specific record.
- Log access-control failures and alert on spikes. A burst of
403/404from one account is a probing signal.
Testing for broken access control
These bugs hide from unit tests because the code "works" for the authorized user; the failure only appears when a different user hits the same endpoint. Effective testing means exercising each endpoint with multiple roles and with objects the current user does not own. Dynamic scanners automate part of this by replaying requests with swapped identifiers and tokens; our overview of dynamic application security testing covers how that works against a running app, and the deeper authorization material lives in the Safeguard Academy.
FAQ
How do you define broken access control?
It is any weakness that lets a user act outside their intended permissions — reading another user's data, invoking an admin function, or escalating their own role. OWASP lists it as the top web application risk.
What is the most common broken access control example?
The insecure direct object reference (IDOR): changing an ID in a URL or request body to reach a record you do not own, because the server checks authentication but not ownership.
Why is broken access control ranked so high by OWASP?
Because the bugs are widespread, easy to find by simply tampering with requests, and often expose sensitive data directly. Low effort to exploit plus high impact is what drives the ranking.
Can automated tools find broken access control?
Partially. Dynamic scanners catch IDOR and missing-function-level checks by replaying requests as different users, but business-logic authorization flaws often still need manual review by someone who understands the intended permission model.