Broken Access Control is the failure to enforce what an authenticated or anonymous user is actually allowed to see and do, and it sits at number one on the OWASP Top 10 (2021). It is not a single bug but a family of them: insecure direct object references (IDOR), missing function-level authorization, privilege escalation through tampered parameters, and access decisions made only in the UI instead of on the server. OWASP promoted it to the top spot after finding some form of it in 94% of the applications it tested, with more recorded occurrences than any other category. This guide explains what the category covers, why it ranks first, the real incidents that define it, and a concrete pattern for detecting and fixing it.
What OWASP A01 actually covers
A01:2021 maps to 34 Common Weakness Enumerations, but four do most of the damage: CWE-284 (Improper Access Control), CWE-285 (Improper Authorization), CWE-639 (Authorization Bypass Through User-Controlled Key, the formal name for IDOR), and CWE-862 (Missing Authorization). The unifying theme is that authorization is checked in the wrong place, at the wrong layer, or not at all. Access control operates at three levels — route ("can this role reach this endpoint?"), object ("can this user touch this specific record?"), and function ("can this user perform this privileged action?") — and a gap at any one of them produces unauthorized access. What makes the category so stubborn is the mismatch in remediation cost. A missing route guard is a five-line fix; an IDOR baked into how object identifiers flow across a dozen microservices can require reworking the whole identity model.
Why it ranks number one
Broken Access Control took the top slot because modern architectures multiply the number of enforcement points and then re-implement checks inconsistently across each one. Every REST endpoint, GraphQL resolver, mobile client, and internal service is a fresh opportunity to forget a check that a sibling service already got right. The shift toward API-first design made this worse: OWASP's separate API Security Top 10 ranks Broken Object Level Authorization first, precisely because endpoints that expose identifiers directly in the path — think /api/invoices/48213 — turn a single missing ownership check into a mass data exposure the moment an attacker scripts sequential requests. Access control is also intrinsically application-specific business logic, which means generic scanners and web application firewalls rarely catch it. The rule lives in your code, so the bug lives in your code.
Real-world examples
The First American Financial exposure in 2019 remains the textbook case: any document URL's numeric identifier could be incremented to read another customer's bank records and Social Security numbers, with no authentication required, because the ownership check simply did not exist. It leaked roughly 885 million documents. More recently, PaperCut's CVE-2023-27350 (an improper access control flaw, CWE-284) allowed unauthenticated attackers to bypass authentication and reach an administrative code path; it was exploited in the wild by ransomware operators within weeks of disclosure. F5 BIG-IP's CVE-2022-1388 (CWE-306, Missing Authentication for a Critical Function) let attackers reach the iControl REST interface without credentials and run privileged commands. Each of these is the same story at different scales — a privileged operation exposed without the check that should have gated it.
Vulnerable versus fixed code
The most common instance is an endpoint that trusts an identifier from the request without confirming the caller owns the resource.
// VULNERABLE: returns any invoice by id, no ownership check (IDOR / CWE-639)
app.get("/api/invoices/:id", requireLogin, async (req, res) => {
const invoice = await db.invoices.findById(req.params.id);
res.json(invoice); // any logged-in user can read any invoice
});
// FIXED: scope the lookup to the authenticated user's own records
app.get("/api/invoices/:id", requireLogin, async (req, res) => {
const invoice = await db.invoices.findOne({
_id: req.params.id,
ownerId: req.user.id, // enforce ownership at the query layer
});
if (!invoice) return res.status(404).json({ error: "Not found" });
res.json(invoice);
});
The fix denies by default and enforces ownership inside the data query itself, so there is no window where the record is fetched before the check runs. Returning 404 rather than 403 also avoids confirming that the resource exists.
Prevention checklist
- Deny by default: every route, resolver, and action requires an explicit allow decision.
- Enforce authorization server-side at the object level, never in the client or navigation menu.
- Scope every data query to the authenticated principal instead of filtering after retrieval.
- Use unguessable identifiers (UUIDs) so sequential enumeration is not trivial, but never treat that as the access control itself.
- Centralize authorization logic in one policy layer and reuse it across services rather than re-implementing per endpoint.
- Test with at least two low-privilege identities and diff the responses to catch horizontal escalation.
- Log access-control failures and alert on spikes that suggest enumeration.
- Disable directory listing and reject unexpected HTTP methods on sensitive routes.
How Safeguard helps
Safeguard closes the gap between "we have an access-control finding" and "we know whether it matters." When a broken-authorization pattern surfaces — whether in your own handlers or inside a third-party dependency — Safeguard's reachability analysis traces whether the vulnerable code path is actually invoked from an externally reachable entry point, so you triage the checks that sit on your real attack surface first. Our SCA engine surfaces access-control CVEs in dependencies like auth middleware, while Griffin AI correlates those findings against your SBOM and deployment context to rank exploitability. Confirmed issues are validated dynamically by Safeguard DAST, which replays requests as multiple identities to expose IDOR that static tools miss, and Auto-Fix opens a scoped pull request adding the missing ownership or role check. If you are weighing tools, our comparison page shows how reachability-first triage differs from raw alerting.
Frequently Asked Questions
Why is Broken Access Control ranked higher than Injection?
OWASP moved it to number one for the 2021 list because it appeared in more tested applications than any other category — some form of it turned up in 94% of them — and because modern API-driven architectures create far more enforcement points to get wrong. Injection dropped to third as parameterized queries became standard, while access control remained a per-application logic problem that no framework solves for you.
What is the difference between authentication and authorization failures?
Authentication answers "who are you?" and authorization answers "what are you allowed to do?" Broken Access Control (A01) is an authorization problem: the user is correctly identified but reaches data or actions they should not. Authentication failures are covered separately under A07. A perfectly authenticated user can still trigger an IDOR.
Can a web application firewall stop broken access control?
Generally no. A WAF inspects request patterns for known attack signatures, but an IDOR request looks identical to a legitimate one — the only difference is whether the caller owns the referenced object, which the WAF cannot know. Access control must be enforced in application code with knowledge of the session and the resource.
How do I test for IDOR specifically?
Create two accounts at the same privilege level, capture a request that references an object owned by the first account, then replay it with the second account's session. If you receive the first account's data, you have an IDOR. Automating this across every object-referencing endpoint, as Safeguard DAST does, is the only reliable way to cover a large API surface.
Ready to find broken access control before an attacker does? Start scanning at app.safeguard.sh/register, or read the setup guide at docs.safeguard.sh.