Broken access control
CWE-284OWASP A01:2021Updated August 31, 20266 min read
Broken access control is the vulnerability class where a user reaches data or functions outside their permissions because the application never enforces that boundary on the server. Horizontal escalation reaches another user's data, vertical escalation reaches higher privileges. The fix is to deny by default and check, at every entry point, who may do what to which object.
Almost every application has users with different permissions: a customer sees their own invoices, an employee sees everyone’s, an administrator changes roles. Broken access control is the umbrella term for every situation where that separation is not enforced in the only place that counts, the server. This article explains how the flaw arises, what an attacker gains from it and how to prevent it structurally.
What is broken access control?
Broken access control is the vulnerability class in which a user can read data or perform actions outside their granted permissions, because the application does not enforce that boundary, or enforces it in the wrong place. The class has held the top position in the OWASP Top 10 since 2021, and it is the umbrella above several better known names: IDOR (insecure direct object reference), privilege escalation and forced browsing are all specific forms of it.
Think of a hotel where every key card opens every door on the corridor. Reception tells you which room is yours, but the lock itself only checks that you hold a card, not which one. As long as everybody follows the signs, nothing looks wrong.
Access control fails in two directions. In horizontal escalation the attacker stays at their own level but reaches data belonging to another user with the same role: they increment the invoice number in the URL and receive another customer’s invoice. That is the classic IDOR case. In vertical escalation they gain privileges. An ordinary user calls an administrative function, approves their own request, or grants themselves a role they should never have been able to obtain.
Alongside those sits forced browsing: an attacker requests a path or endpoint that appears nowhere in the interface, such as an export function under an admin path or an old API version that was never switched off. Not seeing the button does not stop anyone from calling the route behind it. An interface that hides menu items is doing presentation work; it is not access control.
How does an access control attack work?
Take a billing portal. Customers log in and see their own invoices, administrators can change roles. The developer carefully checks that someone is logged in, but never checks what that someone is entitled to.
Vulnerable:
// requireLogin only verifies that a valid session exists
app.get("/api/invoices/:id", requireLogin, async (req, res) => {
const invoice = await db.invoices.findById(req.params.id);
res.json(invoice);
});
// The button only appears in the admin interface, so the route looks safe
app.post("/api/users/:id/role", requireLogin, async (req, res) => {
await db.users.update(req.params.id, { role: req.body.role });
res.sendStatus(204);
});
The attacker holds an ordinary customer account. They open their own invoice, notice the number 10431 in the URL and try the one next to it:
GET /api/invoices/10432 HTTP/1.1
Host: portal.example.com
Cookie: session=eyJhbGciOi
The server looks up the invoice, finds it and returns it. Nowhere did anything check whether invoice 10432 belongs to this user. A short script walks the whole identifier range, and within an hour the attacker holds the complete billing history with names, addresses and amounts.
The second route goes further. It is linked nowhere in the customer interface, but it sits in the JavaScript bundle every visitor downloads. The attacker composes the request by hand:
POST /api/users/8891/role HTTP/1.1
Host: portal.example.com
Content-Type: application/json
Cookie: session=eyJhbGciOi
{"role":"admin"}
The application establishes only that a session exists and applies the change. That is vertical escalation in a single request.
The structural fix has two parts. First, deny by default: a route without an explicit authorization decision never gets past the middleware. Second, check more than the role, and verify the relationship between the user and the concrete object.
Secure:
// Deny by default: every route must claim a permission explicitly
app.use(requireLogin);
app.use(denyUnlessAuthorized); // rejects anything without authorize()
app.get("/api/invoices/:id", authorize("invoice:read"), async (req, res) => {
const invoice = await db.invoices.findById(req.params.id);
// Object level: does this invoice belong to this user?
if (!invoice || invoice.ownerId !== req.user.id) {
return res.sendStatus(404);
}
res.json(invoice);
});
// authorize reads permissions from the server-side session, never from the request
app.post("/api/users/:id/role", authorize("user:manage"), async (req, res) => {
await db.users.update(req.params.id, { role: req.body.role });
res.sendStatus(204);
});
Requesting invoice 10432 now returns a 404, and the role change is refused for anyone without the user:manage permission. What matters most is where authorize gets its information: from the server-side session or a verified token, never from a header, a hidden form field or a parameter carrying a user identifier. Anything the client sends, the client can change.
What is the impact of broken access control?
Impact ranges from high to critical, depending on what sits behind the missing check. At the lighter end is reading other users’ data: personal details, invoices, case files, messages. That is almost always a reportable data breach, and an incrementing sequence of identifiers yields the entire dataset rather than a single record.
At the heavier end is vertical escalation. An attacker who makes themselves an administrator can modify or delete data, take over other accounts and often reach functionality that opens the system further. Business logic fraud belongs in the same category: granting a discount or raising a limit reserved for another role. Because an ordinary HTTP request is enough, the barrier is low and the traces left in your logs are hard to distinguish from normal behaviour.
How do you detect broken access control?
This class does not automate reliably, because a scanner has no idea who is supposed to see what. Testing therefore starts with a permission matrix: which roles exist, what each role may do, and which endpoints implement them.
The practical method uses at least two accounts. Capture the requests made by the account with the most privileges, replay them with the session of the least privileged account, and see whether the response changes. Next, swap identifiers in URLs, JSON bodies and headers for those of the other user. Add the variations: the same route with a different HTTP method, or a path you only know from the JavaScript bundle or outdated API documentation.
Automation does help in finding unprotected admin paths, but the judgement stays human. AssistSec tests access control during a penetration test using several accounts per role, and shows for each finding which request returned which data.
How do you prevent broken access control?
- Deny by default. Have the framework reject every request unless an explicit authorization decision has been made. A new route is then closed until someone deliberately opens it.
- Authorize server-side at every entry point. Every route, every API, every background job and every GraphQL resolver. Checks in the frontend are convenience, not security.
- Check the object, not only the role. Ask on every request whether this user is entitled to this specific record, and put that ownership condition inside the query rather than after it.
- Never trust identity or role from the request. Derive the user and their permissions from the server-side session or a verified token, never from a parameter, a cookie field or a hidden input.
- Centralise the decision. A single authorization component called from everywhere can be reviewed and tested; checks scattered through the code will eventually be missing somewhere.
- Log denied attempts and test for them. Alert on repeated authorization failures, and cover access control in your automated tests with a case per role.
Sources
Frequently asked questions
What is the difference between horizontal and vertical privilege escalation?
In horizontal escalation the attacker stays at their own privilege level but reaches data belonging to another user with the same role, such as another customer's invoice. In vertical escalation they gain higher privileges and perform an action reserved for a more powerful role, such as assigning themselves an administrator role. Both fall under broken access control, and both are prevented in the same place: a server-side check on every request.
Is IDOR the same as broken access control?
IDOR is one specific form; broken access control is the class around it. In an insecure direct object reference, a parameter points straight at an object, typically an identifier in the URL, and the application never checks whether that object belongs to the requesting user. Privilege escalation and forced browsing sit in the same class but take a different route.
Why is authentication not enough?
Authentication establishes who someone is; authorization decides what they may do. An application that only verifies that a valid session exists lets every logged-in user reach everything the application can do. In practice this is the most common root cause of broken access control.
Does hiding an endpoint from the menu protect it?
No. A hidden button or a menu item rendered only for administrators is presentation, not security. The path is usually visible in the JavaScript bundle, in API documentation or in an older version of the application, and an attacker simply calls the route directly. This is known as forced browsing.
Can a scanner find broken access control?
Only partly. A scanner sees that a request returns a 200 response, but it does not know which user is supposed to see which object; that knowledge lives in your business logic. Automated testing mostly finds unprotected admin paths and missing authentication, while manual testing with several accounts exposes the rest.
Related articles
- GlossaryPentestA penetration test (pentest) is a controlled attack on your systems by ethical hackers. Learn how a pentest works and what vulnerabilities it uncovers.
- VulnerabilitiesCWE-287A07:2021Broken authenticationBroken authentication explained: how attackers take over accounts through brute force, leaked passwords and predictable session tokens, and how to stop them.
- VulnerabilitiesCWE-639A01:2021Insecure direct object reference (IDOR)IDOR explained: how attackers tamper with IDs in URLs or APIs to read or change other users' data, and how to detect and prevent this vulnerability.
- VulnerabilitiesCWE-269A01:2021Privilege escalationPrivilege escalation explained: how attackers gain admin rights through a client-trusted role field or an unprotected admin route, and how to stop it.