A user changes the id in a URL from 1024 to 1025 and receives an invoice their account was never authorized to see. A standard user sends a POST request to the admin-only /admin/users endpoint and receives a 201 response. Neither request exploits a clever bug, and neither one needs a payload. Both are syntactically valid. The vulnerability is rooted in the authorization decision that never happened.
This scenario perfectly encapsulates broken access control and why it sits at the top of the OWASP Top 10. OWASP’s A01 spans 40 CWEs, including path traversal (CWE-22), CSRF (CWE-352), and SSRF (CWE-918), and its description reaches further still, into CORS misconfiguration. This guide covers authorization failures for users, roles, tenants, objects, fields, and workflows in web applications and APIs.
What Is Broken Access Control?
Broken access control is a security failure that happens when an application doesn’t properly enforce what a consumer may do to a resource. OWASP puts it plainly: access control enforces policy so users cannot act outside their intended permissions. When it fails, the result is unauthorized information disclosure, modification, or destruction of data, or a business function performed outside the caller’s limits.
Authentication answers “who are you?” Authorization answers “what is this identity allowed to do?” Broken access control is an authorization failure, and it isn’t only about logged-in humans: OWASP’s ASVS defines authorization as granting access “only to permitted consumers (users, servers, and other clients).” An anonymous caller can be legitimately authorized to reach a public resource, which is why OWASP’s prevention guidance reads “except for public resources, deny by default.”
That distinction changes what you’re looking for. The caller in a broken access control scenario is usually logged in and often a paying customer, so nothing about the session looks unusual. A valid session establishes the identity the application currently associates with the request. It says nothing about whether that identity may perform this operation on this resource, and the application never asked.
Why Broken Access Control Tops the OWASP Top 10
Broken access control moved to the number one slot in the OWASP Top 10:2021, and it stayed there in the 2025 edition. The numbers behind that ranking are worth reading carefully, because the two most-quoted figures in this category both get misread, and one of them gets misread on OWASP’s own page.
| Metric (OWASP contributed dataset) | 2021 A01 | 2025 A01 |
| CWEs mapped | 34 | 40 |
| Average incidence rate | 3.81% | 3.74% |
| Max incidence rate | 55.97% | 20.15% |
| Average coverage | 47.72% | 42.93% |
| Max coverage | 94.55% | 100.00% |
| Total occurrences | 318,487 | 1,839,701 |
| Total CVEs | 19,013 | 32,654 |
OWASP defines both metrics in the methodology notes for the 2025 edition. Incidence rate is the percentage of applications found vulnerable to a CWE out of those tested for it. Coverage is the percentage tested for it at all, reported both as an average across contributors and as a maximum for any single contributor. Substituting one for the other is where the misreadings come from.
Look at the 2021 column. The much-quoted “94%” is that year’s max coverage, 94.55%, meaning the highest share of any one contributor’s applications tested for the category. It is not the share found to have it. Average coverage was 47.72%, and the prevalence figure is the average incidence rate of 3.81%.
The 2025 page makes the same swap, except this time OWASP makes it itself. The opening line says 100% of applications tested were found to have some form of broken access control. The score table shows the same 100.00% under max coverage, compared with 42.93% average coverage and 3.74% average incidence. OWASP’s own introduction to the 2025 edition cites the table: on average, 3.73% of tested applications had one or more of the 40 CWEs in the category. Take the table and the introduction over that opening line.
Occurrence counts need the same care. They rose from 318,487 to 1,839,701. But the contributed dataset grew from over 500,000 applications to over 2.8 million across a largely different set of contributors, and the category expanded from 34 CWEs to 40. Most of that jump is the dataset, not the industry.
The Three Types of Access Control Failures
A practical way to organize authorization tests is by the boundary they cross: vertical, horizontal, or context-dependent.
Vertical Privilege Escalation
Vertical privilege escalation occurs when a standard user gains the privileges of a higher-privileged user. A common example: an admin action is hidden in the UI, but its endpoint has no server-side role check, so anyone who knows the route can call it directly. Missing checks are only one route in, alongside incorrect role decisions and manipulable authorization metadata. MITRE files that omission as CWE-862, Missing Authorization, where the product doesn’t perform an authorization check when an actor tries to access a resource or perform an action.
Horizontal Privilege Escalation
In horizontal privilege escalation, the attacker acquires no higher role. Both accounts are at the same nominal privilege level, and swapping 1024 for 1025 in the request still returns another user’s data. That’s where insecure direct object references live. MITRE has a dedicated weakness for it: CWE-639, Authorization Bypass Through User-Controlled Key, in which authorization fails to prevent one user from accessing another user’s record by modifying the key that identifies them.
Context-Dependent Failures
Context-dependent failures occur when the policy depends on state, sequence, time, or prior approval, and the application doesn’t enforce those conditions. Take a checkout whose final step requires an earlier step to be approved: accepting that request without the state transition violates the policy, even though the request is well-formed. Context-dependent access controls are easy to miss because no single request looks wrong in isolation.
In API terms, the same patterns carry different names. Broken Object Level Authorization (BOLA) is an API failure to verify that the caller may perform the requested action on the referenced object, covering unauthorized reads, writes, and deletes rather than reads alone. Broken Function Level Authorization (BFLA) covers operations you shouldn’t reach at all. The terms overlap without lining up: MITRE treats IDOR as broader than CWE-639 because it also covers path traversal, and its maintenance note says the CWE team suspects BOLA extends beyond directly referenced objects.
Real-World Examples of Broken Access Control
Unfortunately, this set of vulnerabilities is quite common and spans quite a few different entry points:
- Parameter tampering. An authenticated request to
GET /api/invoices/1024returns your invoice. Changing it toGET /api/invoices/1025returns a stranger’s invoice because the server looks up the invoice by ID and never checks ownership. OWASP describes this as permitting viewing or editing someone else’s account by providing its unique identifier. - Forced browsing. The admin dashboard link is hidden from your account, but
GET /admin/reportsstill renders because the route only checks that you’re logged in, not that you’re an admin. OWASP defines force browsing as guessing URLs to reach authenticated pages as an unauthenticated user, or privileged pages as a standard user. - Method-based gaps.
GET /api/users/42is protected, butDELETE /api/users/42never got the same check, so a lower-privileged user can delete records they can’t even view. OWASP calls out missing access controls on POST, PUT, and DELETE specifically. - Unauthorized property modification. A profile endpoint binds request fields automatically, so a standard user can add
"role": "admin"to the JSON body and promote the account. The binding creates the path; the missing field-level check creates the escalation. Worth knowing where this fits: API3:2023 Broken Object Property Level Authorization in the API Top 10, but CWE-915 under A08 rather than A01 in the web Top 10. - Client-side-only enforcement. The admin route is hidden by JavaScript, so it looks locked in a browser. OWASP’s own scenario is blunt about the fix: the attacker just runs
curl https://example.com/app/admin_getappInfofrom the command line.
None of these require breaking encryption or injecting code. They require noticing that a check is missing and sending the request anyway. And then, boom, vulnerability exploited without a whole lot of effort.
How to Detect Broken Access Control
Finding access control vulnerabilities is a different problem from listing them, and the tooling story is more nuanced than “scanners can’t do it.” While it’s true that most scanners and testing platforms can detect at least some broken access vulnerabilities, it’s important to know what tool/method to deploy to get the coverage you need.
One of the most important things is to test only applications you own or have written authorization to assess, using dedicated identities and representative test data in an environment where reads, writes, and deletes are permitted by your rules of engagement. Should you test against production? In most cases, probably not.
What Code Review Can Flag
MITRE lists automated static analysis as a highly effective method for finding some instances of CWE-639 by modeling data and control flow from attacker-controlled input to sensitive operations. CWE-862, Missing Authorization, is a class-level weakness covering the absent check rather than the manipulable key. MITRE rates the same method as Limited because tools struggle with custom authorization schemes and functionality meant to be accessible to anyone. The gap between those ratings is the root of the problem. A key flowing from request to query is a pattern a tool can model. Whether this caller is entitled to that record is a policy question that the source alone usually doesn’t answer.
Anthropic’s security guidance plugin for Claude Code illustrates the “good” case. Its documentation describes an agentic commit review that reads related files to trace data flow across the codebase, naming IDOR and auth bypass among the multi-file vulnerabilities that pattern matching misses. The plugin’s own documentation also sets the boundary: findings are suggestions, not a substitute for human code review, SAST/DAST, dependency scanning, or pen-testing. Good for a first step, but not enough for final sign-off.
What Runtime Testing Can Verify
Whether a caller may read invoice 1025 depends on the policy governing that invoice and on who is asking, both of which require live data and live configuration. A reviewer reading GET /api/invoices/:id sees a lookup that looks correct, because the code is often correct in shape and wrong only in what it omits.
Runtime testing closes another gap: it exercises a known policy boundary using representative identities and resources against the deployed application. Neither method is complete alone. Source review misses behavior introduced by configuration and integrations, and a dynamic test only covers what it actually exercises. That’s why OWASP asks for functional access control tests in unit and integration suites, and why teams still test the running application.
Manual Replay Versus Automated Cross-Identity Testing
The manual method is straightforward. Create a resource as user A and confirm the policy does not authorize user B to reach it. Then replay the read, update, or delete as user B, checking both the response and the resulting state. A 403, or a deliberately opaque 404, is the expected denial. One replay is a test rather than coverage; a real assessment walks a matrix across identities, roles, tenants, objects, fields, and methods.
Automating it hits a constraint that catches teams out. A scan that authenticates once and crawls using that single session has no second identity to attempt with, so it can’t determine whether another identity crosses the boundary. Cross-identity testing requires multiple authenticated profiles, the expected policy context, and representative resources for each. That setup difference is part of why business logic vulnerabilities survive scans tuned for injection and XSS.
Testing Authorization Inside an AI Coding Workflow
That multi-identity requirement is what StackHawk’s multi-profile Business Logic Testing is built for. In its multi-profile configuration, HawkScan crawls your API using each configured profile and captures resource identifiers, such as user IDs and order IDs, from the responses. It then attempts to read one profile’s resources using another profile’s credentials, testing for BOLA, and attempts mutating operations across profile boundaries, testing for BFLA. An isPrivileged: true key on a profile marks an administrative or otherwise elevated account, so HawkScan can test whether unprivileged users reach its privileged-only endpoints and resources.
Setup cost varies by depth. Low-configuration plugins cover multi-tenant isolation, Broken Object Property Level Authorization (BOPLA), and BFLA; they’re off by default and switch on through policy management. Cross-user BOLA testing needs the next level of configuration: multi-profile testing. That asks for an OpenAPI specification, at least two test accounts with valid credentials, and a test environment that reflects production data patterns so cross-user attempts are meaningful. Because these scans simulate genuine cross-user access attempts, run them in staging or test environments, never against production systems that hold real customer data.
When the code is agent-written, the same tests can run inside the agent loop. Agent skills cover five agents, and their installations differ depending on which one(s) you need. Claude Code, Codex, and GitHub Copilot can be installed from the plugin marketplace. Cursor copies rule files into .cursor/rules/, and Antigravity installs the plugin from our stackhawk/agent-skills GitHub URL.
For existing StackHawk users, it’s important to note that you’ll need the hawk CLI v6.0.0 or later and, just like those who’ve used our DAST offering, a locally running application that the scanner can reach, along with its source. Finish a feature, and the agent runs the scan, interprets the findings with full codebase context, fixes what it finds, and rescans to verify. That puts the authorization check next to the code that created the endpoint, which matters when an agent writes routes faster than anyone reviews them.
How to Prevent Broken Access Control
Many of the mitigation techniques here are actually pretty simple; however, they can be easily missed. OWASP’s own guidance is a solid place to start:
- Deny by default. Everything except intentionally public resources should require an explicit grant, so new endpoints inherit “no access” until you decide otherwise.
- Enforce on the server, always. OWASP is unambiguous that access control only works in trusted server-side code or serverless APIs, where an attacker can’t modify the check or its metadata.
- Enforce resource-level policy in the model. OWASP asks that access control models enforce record ownership rather than letting users create, read, update, or delete any record. Ownership is the common case, not the only one: tenancy, delegation, and resource state are inputs too.
- Build the mechanism once and reuse it. Scattered per-route checks are how gaps appear. OWASP recommends implementing access control once, reusing it application-wide, and leaning on well-established toolkits that provide declarative controls.
- Get session management right. Invalidate stateful session identifiers on the server after logout, and keep stateless JWTs short-lived. A short expiry shrinks the window; it doesn’t make the authorization decision correct or grant immediate revocation.
- Log decisions and limit automated abuse. Record denied operations with enough subject, resource, action, and policy context to investigate repeats, and rate-limit API and controller access so enumeration is slower and noisier. Rate limiting slows exploitation; it doesn’t repair a missing check.
- Test the authorization matrix in the pipeline. OWASP closes by telling developers and QA staff to include functional access control in their unit and integration tests. Cover anonymous and authenticated callers, peer identities, roles, tenants, protected objects and fields, and every supported method.
Final Thoughts
Broken access control stays at the top of the OWASP Top 10 because it’s a logic problem wearing a code problem’s clothes. Clean code and a broken application are entirely compatible. The question is whether a given identity can access a given resource, and the answer lies in your policy, your data, and your configuration rather than in any one file.
Deny by default, enforce on the server, and test the boundaries you documented. If you want to see what automated cross-identity testing looks like against a running application, that’s what we built Business Logic Testing to do. Want to check out how our Agentic Runtime Testing solution works for yourself? Sign up now to give it a try.
Frequently Asked Questions
Add the missing decision at the trusted enforcement point, so the server confirms the caller may perform this operation on this resource. Check the sibling routes and other methods on the same endpoint, because the gap is rarely the only one. Then add a regression test using the affected identities and resource.
Insecure direct object references, where changing an ID in a request returns another user’s data. And forced browsing, where an admin-only endpoint responds to a standard account because the route checks for login rather than role. Both look ordinary in basic HTTP access logs, because both requests are syntactically valid. Logging the authorization decision, not just the request, is what makes them visible.
Broken authentication is a failure to correctly verify a caller’s identity. Broken access control is a failure to enforce what a caller may do, whether that caller is authenticated or anonymous. In most broken access control findings, the user is logged in legitimately, which is why the requests look so ordinary.
Avoiding it is an architecture question more than a patching one. Document the subject, resource, operation, and contextual policy, deny anything that doesn’t match, and centralize enforcement so new endpoints inherit the control. Then test that matrix deliberately, rather than whatever a crawl happens to touch.