Broken access control is the single highest-ranked risk in the OWASP Top 10 — and has been for every revision of the list since 2021. The reason is not subtle: access control is the hardest of the major security categories to get right, the most stack-specific, and the category where the failure mode is almost always a quiet "an authenticated user reached data or operations they should not have" rather than a dramatic exploit. Broken access control owasp — A01 — shows up as IDOR in API responses, as privilege-escalation holes in admin panels, as forgotten role checks in newly added endpoints, as JWT claims that are trusted when they should be verified, as forced-browsing attacks that succeed because the authorization check was forgotten. This guide walks what broken access control actually is, the patterns every 2026 pentest report finds variants of, concrete examples of each in modern web and API stacks, the testing methodology that catches the issues before production, and the engineering playbook that turns authorization from a recurring incident category into a property of the architecture.
What Is Broken Access Control?
Access control is the mechanism that decides whether an authenticated user is allowed to perform a specific operation on a specific resource. Authentication is "who are you"; access control — also called authorization — is "what are you allowed to do." Broken access control is the vulnerability class that arises when the authorization check is missing, incorrect, or bypassable. The authenticated user reaches a resource they should not reach, performs an operation they should not perform, or obtains privileges they should not have — not because the authentication was bypassed, but because the authorization layer on top of authentication failed.
The distinction matters because the mitigations are different. Authentication failures are fixed with stronger auth protocols, MFA, session management, and credential hygiene. Access control failures are fixed with architectural choices about how authorization decisions are expressed, where in the request path they are enforced, and how the application's data model enables or frustrates the checks. A system with perfect authentication and broken authorization is catastrophically insecure — every logged-in user can potentially do every operation — and the authentication layer, however well-engineered, does not save it.
The OWASP Top 10 has ranked access control as A01 — the top risk — since the 2021 revision, and the 2025 list keeps the ranking. The data justifying the ranking is consistent: access control findings appear in the majority of pentest reports, account for a disproportionate share of bug bounty payouts, and show up in nearly every major breach post-mortem that involves data exposure rather than infrastructure compromise. The ranking reflects prevalence, severity, and the difficulty of remediation — the three properties that make a vulnerability category operationally expensive.
How OWASP Defines A01: The Top-Ranked Risk
The formal OWASP definition of owasp broken access control enumerates a specific list of failure conditions. Every pentest report, every automated scanner, and every compliance audit will reference this list, so understanding each is worth the paragraph.
| OWASP failure | What it means in practice |
|---|---|
| Violation of least privilege | Default access is broader than required; allowlist becomes denylist |
| Bypassing access checks by URL modification | Path traversal, ID tampering, direct object reference |
| Permitting viewing/editing others' accounts | IDOR — insecure direct object reference |
| Accessing APIs without access control | Missing authorization at the API endpoint level |
| Elevation of privilege (vertical) | User becomes admin; regular user reaches admin-only operations |
| Metadata manipulation | JWT tampering, cookie rewriting, hidden-field tampering |
| CORS misconfiguration | Cross-origin access permitted where it should be denied |
| Forced browsing | Accessing authenticated pages by guessing or enumerating URLs |
Each of these failure modes has a specific remediation pattern, and a mature program addresses each explicitly. The failure mode that dominates in practice, however, is IDOR — insecure direct object reference — which accounts for the single largest share of access-control findings in both pentest data and bug-bounty leaderboards. The other failure modes show up frequently enough to be named, but IDOR is the category that every reviewer looks for first.
The Five Patterns of Broken Access Control
The OWASP enumeration above is formal. The patterns that engineering teams meet in production collapse to a smaller set of five recurring shapes. Each is preventable; each recurs when the prevention is not institutional.
1. IDOR — Insecure Direct Object Reference. An endpoint accepts an identifier as input (a user ID, a document ID, an order ID) and returns the resource corresponding to that identifier without checking whether the authenticated user is authorized to access it. An attacker modifies the identifier in the request — from GET /api/orders/1001 to GET /api/orders/1002 — and reads a resource belonging to another user. IDOR is the single most common access control vulnerability in 2026, and its prevalence traces to a specific architectural mistake: the application trusts the identifier in the request rather than deriving the accessible resource from the authenticated session.
2. Missing function-level access control. A newly-added endpoint lacks the authorization check that exists on comparable older endpoints. The endpoint is an admin operation but is accessible to any authenticated user because the route was created, shipped, and forgotten by the authorization layer. The pattern is especially common when authorization checks are implemented per-endpoint rather than centrally — each endpoint's author has to remember to add the check, and any one forgetting breaks the system.
3. Vertical privilege escalation. A regular user is able to perform an operation reserved for users with a higher role — admin, moderator, staff. The escalation can happen through a missing function-level check (see pattern 2), through metadata tampering (changing a role claim in a JWT), through a race condition during role transitions, or through a feature that was designed to be role-restricted but whose restriction was implemented on the client side only.
4. Horizontal privilege escalation. A user is able to perform operations that are authorized at their role level but scoped to a different user's resources. User A edits User B's profile, reads User B's messages, or cancels User B's order. The failure is that the application correctly checks "is the user authorized for this operation class" but fails to check "is the user authorized for this specific resource." IDOR is typically a horizontal escalation; the distinction between "IDOR" and "horizontal escalation" is semantic rather than operational.
5. Forced browsing and metadata tampering. An attacker accesses resources by guessing or enumerating URLs, manipulating hidden form fields, or tampering with cookies, tokens, or headers that encode authorization claims. The defense is to treat every request as potentially hostile and re-verify authorization on the server, not to rely on any client-side enforcement or on obscurity of URLs.
IDOR: The Single Most Common Access Control Failure
IDOR earns its own section because its share of real-world findings is so large. The pattern is consistent: an endpoint accepts a resource identifier as input, looks up the resource, and returns it — without checking that the authenticated user has a relationship to the resource that justifies access.
The classic pattern. An API endpoint GET /api/users/{userId}/orders/{orderId} takes a user ID and an order ID and returns the order. The application validates that the caller is authenticated, looks up the order by orderId, and returns it. The caller's own user ID is never compared against the userId in the URL, and the order's owner is never compared against the caller. An attacker sets userId and orderId to values that belong to another user; the query succeeds; the response leaks.
The architectural fix. The application does not accept the resource identifier from the URL as trusted input. Instead, the authenticated session identifies the user, and the query derives the accessible resources from the session. An endpoint GET /api/orders/{orderId} still looks up the order by ID, but the lookup is constrained by the authenticated user: SELECT * FROM orders WHERE id = ? AND user_id = ?. If the order does not belong to the user, the query returns no rows, and the application returns 404 (or 403, with tradeoffs discussed below). The identifier is still in the URL, but the authorization is derived from the session, not the URL.
The 404 vs 403 decision. When an IDOR attempt is blocked, should the response be 404 (resource not found) or 403 (forbidden)? The tradeoff: 404 avoids confirming to the attacker that the resource exists (which is itself information leakage — username enumeration, order-ID enumeration); 403 is more honest about the failure mode but leaks the information. The prevailing recommendation in 2026 is 404 for resources where existence is itself sensitive (payment records, medical data, private documents) and 403 for resources where existence is public but access is restricted (published articles with private edit operations). The decision is context-dependent and should be documented in the API's security conventions.
UUIDs are not a fix. A common misunderstanding is that using UUIDs instead of sequential integers for resource IDs prevents IDOR. UUIDs make enumeration harder but do not make it impossible — references leak through logs, API responses, shared links, and bug reports. Treating UUID adoption as an IDOR mitigation is the classic security-through-obscurity mistake. Use UUIDs for the unrelated reason that they are database-friendly; fix IDOR through authorization checks, not identifier choice.
GraphQL and batch endpoints. GraphQL APIs and batch-request endpoints have a pattern-specific IDOR risk: the request includes a list of identifiers, and the resolver iterates over them and returns the matched resources. If the authorization check is per-request rather than per-identifier, the attacker can include arbitrary IDs in the list and receive data for all of them. The mitigation is to apply the authorization check inside the resolver for each identifier, or to express the query such that the database-level filter includes the user constraint for every lookup.
Function-Level and Role-Level Access Control
Where IDOR fails horizontally (user accessing user's resources), function-level failures fail vertically (regular user accessing admin functions). The pattern's prevalence traces to a specific anti-pattern: decentralized per-endpoint authorization enforcement.
The decentralized pattern. Each endpoint's author is responsible for including the appropriate authorization check at the start of the handler. The admin-delete-user endpoint starts with if (!user.isAdmin) return 403;. The admin-view-audit-log endpoint starts with the same check. The admin-billing-export endpoint starts with the same check. The new endpoint added last week for admin-feature-flag — shipped by a developer who did not notice that the convention exists — has no such check, and every authenticated user can toggle feature flags.
The centralized alternative. Authorization is expressed declaratively at the route level — a middleware, a decorator, or a framework-level policy that maps routes to required permissions. The developer declares the route's requirement (@require_permission("admin:feature-flags")); the framework enforces it. The developer cannot ship an endpoint without a declared permission because the framework rejects it; the default behavior on a missing declaration is deny, not allow. This pattern turns "forgot the check" from a security incident into a compile-time or startup-time error.
RBAC vs ABAC vs ReBAC. Three competing models for expressing authorization. Role-Based Access Control (RBAC) assigns roles to users and permissions to roles — simple, widely understood, but struggles with fine-grained or context-dependent authorization. Attribute-Based Access Control (ABAC) evaluates policies against user attributes, resource attributes, and environmental attributes at request time — more expressive, more operationally complex. Relationship-Based Access Control (ReBAC), popularized by Google's Zanzibar and its open-source descendants (SpiceDB, OpenFGA, Permify), models authorization as relationships between subjects and objects and answers queries like "does user X have permission Y on object Z" — well-suited to applications with complex sharing semantics (documents shared with teams, projects with members, folders with inherited permissions).
The 2026 pattern. Most greenfield applications in 2026 start with RBAC and migrate to ReBAC when sharing semantics become complex enough to justify the investment. ABAC is less common in general-purpose applications but remains the standard for regulated environments (finance, healthcare) where context-dependent policies (time-of-day restrictions, location-based access, data-classification-driven access) are material.
Vertical vs Horizontal Privilege Escalation
The two escalation directions deserve separate treatment because the defenses differ.
Vertical escalation. A regular user reaches a higher-privilege role. The vectors include: (a) missing function-level access control on admin endpoints; (b) JWT claims that encode the role but are not verified or are verified with a weak algorithm; (c) session fixation where the session's role claim can be set by the client; (d) race conditions during role promotion or demotion; (e) self-service role requests that are inadvertently granted without approval; (f) indirect paths through features that delegate privilege (a shared admin token, a webhook that executes with elevated privileges).
The primary defense against vertical escalation is centralized, declarative authorization at the endpoint level — discussed above — combined with secure role storage. Roles should be stored server-side, referenced from the session, and never trusted when they arrive as a client-supplied claim. Systems that encode roles in JWTs must verify the JWT signature with a secure algorithm and reject tokens with disabled algorithm fields (the "alg: none" attack).
Horizontal escalation. A user at a given role reaches another user's resources at the same role. IDOR is the primary vector, but horizontal escalation also occurs through shared session keys, insufficient resource ownership checks on update operations, and flows that pass resources between users where the receiving user's ownership is not properly established.
The primary defense against horizontal escalation is resource-ownership verification on every access — the authorization check not only confirms the user's role but also the user's relationship to the specific resource being accessed. The pattern scales best when the ownership constraint is pushed into the data-access layer (query-level filters, row-level security in the database) rather than added as an ad-hoc check in each handler.
JWT and Token-Based Access Control Failures
JWTs have become the default session and authorization token format for 2026 web and API stacks, and their flexibility has created a family of access control vulnerabilities that are specific to the token-based model.
The "alg: none" attack. A JWT's header specifies the signature algorithm. Some libraries historically accepted alg: none — meaning "no signature" — and treated the token as valid. An attacker crafted a token with alg: none and arbitrary claims, and the server accepted it. The remediation is to configure JWT verification with an explicit allowlist of algorithms and reject any token that specifies an algorithm outside the list. Modern libraries default to safe behavior, but configuration mistakes still occur in the wild.
Algorithm confusion attacks. A JWT issuer signs tokens with RS256 (asymmetric, private-key signing). A misconfigured verifier accepts HS256 (symmetric, shared-key signing) and uses the public key as the symmetric key. An attacker obtains the public key (typically from a JWKS endpoint) and signs an arbitrary token with it; the verifier accepts the token. The remediation is the same algorithm allowlist — explicitly require RS256 or whatever the expected algorithm is.
Trusting unverified claims. A service downstream of the JWT-issuing service extracts claims from the token and uses them for authorization decisions, but does not verify the signature. The assumption is that "the token was already verified upstream." The assumption fails when the service is reached through a path that bypasses the upstream verification — directly, through a misconfigured service mesh, or through a tunnel that legitimate internal traffic uses. Every service that makes authorization decisions from a token must verify the token itself.
Revocation and refresh failures. JWTs are typically valid until their expiration; a token stolen one minute after issuance is usable until expiration even if the user has logged out or had their role changed. The mitigation patterns — token revocation lists, short access-token lifetimes with refresh rotation, server-side session stores — each have tradeoffs, and the right combination depends on the system's risk tolerance. Systems that use long-lived JWTs without a revocation mechanism accept the risk that a compromised token remains usable until expiration.
Claim injection. An attacker adds arbitrary claims to a JWT (role, tenant, scope) that the issuer did not include. If the verifier trusts any claim that is present in the token — rather than trusting only claims the issuer is expected to include — the attacker can escalate privileges. The remediation is that the verifier's policy explicitly names which claims are authoritative from which issuer.
How to Test for Broken Access Control
Access control is notoriously difficult to test automatically because the correct authorization decision depends on the application's data model and business logic, neither of which a generic scanner can infer. The testing patterns that work combine automated coverage with disciplined manual review.
Two-user testing. The baseline method: create two test users (or two test admins, and so on for each role), perform every operation as each, and verify that cross-user operations fail. For an API endpoint GET /api/orders/{id}, log in as User A, create an order, record the ID; log in as User B, attempt to retrieve the order by ID; verify the response is 404 or 403. This two-user pattern, extended to cover every endpoint and every role combination, catches the majority of IDOR and horizontal-escalation findings.
Automated authorization testing in CI. Integration tests that encode the authorization model as a test matrix — for each endpoint, for each role, for each ownership condition, assert the expected allow/deny behavior. Tools like ZAP and Burp Suite Pro can extend this with automated fuzzing of authorization parameters, but the test matrix itself has to come from the authorization model; a fuzzer without the model can only find trivial cases. The test matrix scales with the endpoints times roles times resource-ownership dimensions, which is substantial for a mature application but manageable with code-generation from the authorization declarations.
Manual testing during threat modeling. The threat modeling session is where access control should get structured attention during design. For each endpoint or operation in the design, walk STRIDE's "Elevation of privilege" category and ask: what authorization check enforces this, where is it enforced, how is it tested, what happens when the check is absent? Access control issues found at design time cost a fraction of what they cost to fix after ship.
Code review with an authorization lens. Every pull request that adds or changes an endpoint includes review for authorization — the check is present, the check is correct, the check uses the centralized mechanism rather than an ad-hoc re-implementation, the check covers both vertical (role) and horizontal (resource ownership) dimensions. Code review without an authorization lens consistently misses the class; review with an explicit authorization checklist consistently catches it.
Pentest and bug bounty. External testing captures the cases that internal testing misses — especially business-logic edge cases and multi-step flows where the authorization check is present at each step but the flow as a whole produces an unauthorized outcome. The findings from external testing should feed back into the test matrix so that the next pentest does not find the same class again.
Access Control Is the Hardest OWASP Category to Get Right — and the Hardest to Teach
Injection has a generic fix (parameterized queries) that works across contexts. Broken access control has no generic fix — the correct authorization decision depends on the application's data model, its sharing semantics, and its role structure. That specificity is exactly why language- and context-specific training beats generic awareness content for this category. SecureCodingHub builds the stack-aware, authorization-aware fluency that turns A01 from a recurring pentest finding into something developers catch at code-review time. If you're tired of every pentest producing another IDOR report, we'd be glad to show you how our program changes that input.
See the PlatformMitigation: The Engineering Playbook
The patterns above converge on a specific engineering discipline. Access control is not a tool you buy; it is an architectural decision that shapes how authorization is expressed, where it is enforced, and how it is tested.
Deny by default. The default for every route, every resource, every operation is deny. Access is granted by explicit declaration — a permission attached to a route, a policy evaluated at request time, a relationship recorded in the authorization store. A missing declaration means denial, not access. This inverts the common failure mode (forgot the check → open access) into its safe version (forgot the check → startup error or 403).
Centralize authorization enforcement. The authorization check runs in one place — a middleware layer, a decorator system, a service-mesh policy, or a dedicated authorization service — rather than being re-implemented per-endpoint. Centralization has two properties: every request is subject to the check, and changes to the authorization model propagate to every enforcement point automatically.
Derive resources from the session, not from the request. For IDOR-prevention specifically, the application's data access layer should take the authenticated session as a primary input and return only the resources the session is authorized to access. A developer who writes a query without the session filter is writing a query that can leak across tenants; pushing the filter into the data layer (query helpers that require the session, row-level security in the database) makes the safe path the default.
Verify tokens at every service. Every service that makes authorization decisions from a token verifies the token itself, with an explicit algorithm allowlist and an explicit list of trusted issuers. Token verification is not the upstream service's responsibility alone; it is every downstream service's responsibility.
Write the authorization model as code. The application's authorization model — what roles exist, what permissions they have, what resource-ownership relationships govern access — is expressed in version-controlled code, not in a diagram or a wiki page. The code is the authoritative source; any deviation in the deployed behavior is a bug in the code, not a policy ambiguity.
Test the authorization matrix. A test matrix of (endpoint × role × ownership-condition) with expected allow/deny outcomes runs in CI. The matrix can be generated from the authorization declarations themselves, which keeps it in sync with the model as the model evolves. New endpoints that do not appear in the matrix fail CI.
Train developers on the category's specificity. Access control does not reduce to a generic fix; it requires developers to reason about the application's data model and the user's relationship to each resource. Language- and stack-specific training that walks authorization patterns in the actual framework the developer uses produces stronger outcomes than generic awareness content that explains "check authorization" in the abstract.
Broken Access Control and the Broader Security Program
Access control interacts with almost every other OWASP category, often as a multiplier. A successful SQL injection into a system with strong access control yields less data than the same injection into a system with weak access control. A successful security misconfiguration exposure (an open admin panel, a leaking debug endpoint) is materially worse when the application's authorization model has no defense-in-depth — once the attacker reaches an authenticated session, they reach everything. The organizations with the worst outcomes are typically not the ones with any single category's worst findings; they are the ones where multiple categories compound, and access control is the category that most often sits on the compounding side.
For training programs, the implication is that access control deserves disproportionate curriculum time relative to its single-category share of OWASP attention. A developer who is strong on injection and weak on authorization ships code where injection bugs are rare but the consequences of any bug (including non-injection bugs like SSRF, template injection, or deserialization) are severe. A developer who is strong on authorization ships code where the compounding damage of any other category is limited.
Closing: Access Control Is the Hardest Category to Fix After the Fact
Access control differs from most other OWASP categories in a specific and operationally expensive way: the remediation is architectural. An injection vulnerability is fixed by replacing a concatenated query with a parameterized one — the fix is local, the diff is small, the surrounding code does not need to change. A broken access control vulnerability is rarely fixed that way. The fix typically requires changing how authorization is expressed across many endpoints, or how resource ownership is checked in the data layer, or how roles are stored and verified. The diff is large, the blast radius is large, and the cost of getting the fix wrong is large.
The consequence is that access control repays investment at design time more than any other OWASP category. A system designed with a centralized authorization model, deny-by-default routes, resource-ownership constraints pushed into the data layer, and an auth model that is expressed as code — that system resists the A01 failure modes structurally. A system that started without those patterns and now needs them accepts a multi-quarter architectural migration to get there. The organizations that realize this before the first major incident save themselves the migration; the organizations that learn it from the incident pay the larger price.
Access control is ranked A01 on the OWASP Top 10 because it is the hardest to get right, the easiest to get wrong, and the most expensive to fix after the fact. The engineering disciplines that prevent it are not exotic in 2026 — deny-by-default, centralized enforcement, declarative policy, resource-ownership in the data layer, token verification at every boundary, and authorization-aware training and code review. What is exotic is the institutional commitment to apply all of them consistently, and the developer fluency that makes the disciplines land in actual code rather than in architectural diagrams that describe how the system is supposed to work.