Application Security

Broken Access Control: The Number One Web Vulnerability and How to Fix It

Access control moved to the top of the OWASP Top 10 in 2021. Here is why it is so hard to get right and what a solid authorization architecture looks like.

Alex
Security Architect
6 min read

Broken access control moved from fifth to first place in the OWASP Top 10 for 2021. That shift was not arbitrary — it reflected data showing that 94 percent of tested applications had some form of broken access control. No other vulnerability category came close to that prevalence.

The reason access control is so pervasive is that it is fundamentally a logic problem, not a technical one. You can parameterize a query to prevent SQL injection. You can encode output to prevent XSS. But there is no equivalent mechanical fix for access control. Every authorization decision requires understanding the business rules: who should access what, under which conditions, and at what level.

Why Access Control Breaks

Missing checks. The most common failure is simply forgetting to check authorization. The developer builds a feature, implements authentication, and never adds the authorization check. The endpoint works, the feature ships, and the authorization gap persists.

Inconsistent enforcement. The web interface checks permissions, but the API does not. The main controller checks authorization, but the helper endpoint that serves the same data does not. One developer adds a check; another developer adding a similar endpoint does not know they need to.

Relying on obscurity. "Nobody will guess that URL" is not access control. Sequential IDs, predictable paths, and enumerable resources are found by attackers in minutes.

Client-side enforcement. Hiding menu items or disabling buttons in the UI is not access control. It is a UX feature. The server must enforce every authorization decision because the client is under the attacker's control.

Privilege escalation paths. Users who can modify their own profile might be able to change their role. Users who can create resources might be able to assign those resources to other accounts. Indirect paths to elevated access are harder to identify than direct ones.

Authorization Architecture Patterns

Role-Based Access Control (RBAC)

Users are assigned roles. Roles have permissions. Permissions grant access to resources and operations.

Strengths: Simple to understand and implement. Works well when access patterns map cleanly to organizational roles (admin, editor, viewer).

Weaknesses: Permission explosion in complex systems. When you need "can edit their own posts but not others' posts," RBAC alone falls short. Teams create increasingly granular roles until the system becomes unmanageable.

Attribute-Based Access Control (ABAC)

Access decisions are based on attributes of the user, the resource, the action, and the environment. "A user in the finance department can view financial documents created by their own team during business hours."

Strengths: Handles complex policies that RBAC cannot express. A single policy engine evaluates multiple attributes.

Weaknesses: Complex to implement and debug. Policy evaluation performance can be a concern. Testing all attribute combinations is combinatorially difficult.

Relationship-Based Access Control (ReBAC)

Access is determined by the relationship between the user and the resource. "You can edit this document because you are a member of the team that owns it."

Strengths: Natural fit for collaborative applications. Handles hierarchical permissions well (org admins, team admins, project members).

Weaknesses: Graph traversal performance at scale. Complex relationship models are hard to reason about.

Policy-Based Access Control

A centralized policy engine (like Open Policy Agent or Cedar) evaluates access decisions based on declarative policies. The application sends authorization queries to the engine, which returns allow or deny decisions.

Strengths: Centralized policy management. Policies are auditable, testable, and version-controlled. Changes to access rules do not require application code changes.

Weaknesses: Additional infrastructure. Latency for policy evaluation. Learning curve for policy languages.

Implementation Principles

Deny by default. Every endpoint should be inaccessible unless explicitly authorized. This is the opposite of how most applications are built, where endpoints are accessible by default and authorization is added selectively.

Centralize authorization logic. Do not scatter permission checks throughout controllers. Use middleware, decorators, or interceptors that enforce authorization uniformly. A missed check in one controller is much more likely when each controller implements its own authorization.

Enforce on the server. Every authorization decision must be made server-side. Client-side checks are for UX only.

Check authorization at the data layer. Filtering query results by the user's permissions is more reliable than checking permissions after loading data. A query that includes WHERE team_id IN (user's teams) cannot return unauthorized data, even if the controller logic has a bug.

Use indirect references. Instead of exposing database IDs in URLs (/api/invoices/4523), use per-user references or non-enumerable identifiers. Even better, resolve the reference through the user's authorized scope so an invalid reference returns 404, not 403.

Log authorization failures. When a user attempts to access a resource they are not authorized for, log it. A pattern of authorization failures from a single user suggests an attacker probing for access control gaps.

Common Access Control Vulnerabilities

Insecure Direct Object References (IDOR). Changing an ID parameter to access another user's data. GET /api/orders/1234 returns your order. GET /api/orders/1235 returns someone else's. This is broken object-level authorization.

Function-level authorization bypass. A regular user accessing admin endpoints. POST /api/admin/users/delete should be restricted to admins but is accessible to any authenticated user.

Horizontal privilege escalation. A user accessing resources belonging to another user at the same privilege level. Harder to detect than vertical escalation because both users have the same role.

Metadata manipulation. Modifying JWT claims, cookies, or hidden form fields to change authorization context. If the server trusts client-provided role information, the client can escalate privileges.

CORS misconfiguration. Overly permissive CORS headers allow attacker-controlled websites to make authenticated requests to your API.

Testing Access Control

Manual testing. Authenticate as different user roles and attempt to access each endpoint as each role. Use tools like Burp Suite's Authorize extension to automate request replay with different authentication contexts.

Automated testing. Write integration tests that verify authorization. For every endpoint, test that authorized users get 200 and unauthorized users get 403. Maintain a test matrix of roles vs. endpoints.

Code review. Check every endpoint for an authorization check. Search for endpoints that only check authentication (is the user logged in) without checking authorization (does this user have permission).

DAST. Dynamic scanners can detect IDOR by replaying requests with different authentication tokens. They are less effective at finding function-level authorization issues.

How Safeguard.sh Helps

Safeguard.sh enforces access control at the supply chain level through policy gates. These gates define what components, vulnerability levels, and compliance standards are acceptable for deployment. By enforcing policies on your software components before they reach production, Safeguard.sh adds an authorization layer to your release process — ensuring that only software meeting your security requirements passes through. The platform's role-based access also controls who can modify policies, approve exceptions, and view sensitive security data within your organization.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.