Common Authorization Pitfalls
March 21, 2025
Authentication tells you who someone is. Authorization tells you what they're allowed to do. Most developers implement authentication carefully — login flows, token validation, password hashing. Authorization gets less attention, and the mistakes are different in character: they're harder to spot in code review, they don't fail loudly, and they often stay exploitable for months before anyone notices.
This post covers the most common authorization mistakes, what they allow attackers to do, and why the fixes actually work.
The Threat — When "Who You Are" Isn't Enough
A user is authenticated. They have a valid session token. Your application knows exactly who they are.
That's not enough. The next question — what are they allowed to do? — requires a separate, explicit answer for every action. Applications that answer this question poorly give authenticated users access to resources, operations, and data that should be off-limits.
Broken access control is consistently OWASP's #1 web application vulnerability. It's not because it's technically complex to fix — it's because it's easy to implement inconsistently, and inconsistency is all an attacker needs.
Missing Authorization Checks
The most straightforward pitfall: an endpoint exists, requests reach it, and nothing on the server checks whether the requester is allowed to be there.
This happens often in growing codebases: a new endpoint gets added quickly, the team assumes the authentication middleware covers it, or an "internal" endpoint gets exposed publicly without the security review that went into the main routes.
Consequences: Any user who can discover the endpoint can use it. REST API routes are often predictable (/api/admin/users, /api/reports/export), and JavaScript bundles frequently contain the full list of API routes your application calls. Endpoint discovery is not an obstacle.
Fix: Implement an authorization check on every route. Default to deny — assume no one is allowed unless the code explicitly grants permission. Static analysis tools can flag endpoints that lack authorization middleware. Automated tests that verify authorization is actually enforced (not just present) catch regressions.
Why it works: Server-side checks run in an environment the requester can't modify. Unlike client-side UI, a server that requires a valid permission before proceeding can't be bypassed by disabling JavaScript or sending a direct API call.
Relying on Client-Side Authorization
Hiding UI elements based on a user's role is good for user experience. It's not a security measure.
The pattern looks like this: regular users don't see the "Delete Account" button, so the developer assumes regular users can't delete accounts. The API endpoint behind that button remains accessible — just not via the UI.
Consequences: Attackers don't use your UI. Sending a DELETE request to /api/account/456 requires no special tools — curl, Postman, or any HTTP client works. If the endpoint processes the request without an authorization check, the attacker achieves what the UI was designed to prevent.
Fix: Enforce authorization on every API endpoint, regardless of whether the corresponding UI element is visible or hidden. Treat UI-based access control as a convenience for legitimate users, not a security boundary.
Why it works: API-level enforcement applies regardless of how the request is made. It doesn't matter whether the requester used your frontend, a script, or a browser extension that modified the page. The server check runs unconditionally.
Horizontal Privilege Escalation
A user is authorized to access their own resources. The application checks that the user is authenticated but doesn't verify that the specific resource being accessed belongs to them.
A common manifestation: resource URLs that include predictable identifiers, like /api/orders/1042 or /api/users/88/documents. An authenticated user changes the ID in the URL. The server checks the session token, sees a valid user, and returns the resource — which belongs to a different user.
Consequences: Any user can access, modify, or delete any other user's data by incrementing or guessing resource identifiers. This is OWASP Broken Object Level Authorization (BOLA) — the most common vulnerability in modern REST APIs. Personal data, private content, financial records, private messages — all of it potentially exposed to any authenticated user who probes the API.
Fix: Check resource ownership alongside role permissions. For every request that accesses a specific resource, verify that the authenticated user is authorized for that specific resource instance — not just that they're allowed to access resources of that type. Use non-sequential resource identifiers (UUIDs) to make enumeration harder, though this is defense-in-depth, not a replacement for the ownership check.
Why it works: Ownership validation adds a second dimension to authorization. The user must have the right role and the right relationship to the resource. One condition without the other is insufficient.
Insufficient Granularity
Permissions start simple: user and admin. Admin can do anything. User can read their own stuff. This is fine for an MVP.
The problem is that "admin can do anything" is genuinely dangerous at scale. A single compromised admin account gives an attacker access to everything. Admins also make mistakes — an overpowered admin account can accidentally affect parts of the system that were irrelevant to their task.
Consequences: Excessive privileges increase blast radius. A compromised account with broad permissions causes broader damage than a compromised account with narrow permissions. "Manage content" admin and "manage billing" admin should be different roles — if either is compromised, the damage is contained.
Fix: Design permissions to be specific and combinable. view_analytics, edit_content, manage_billing, and delete_user are more useful than a monolithic admin permission. Use attribute-based access control alongside roles when you need fine-grained per-resource permissions (e.g., "can edit documents they own" rather than "can edit all documents").
Why it works: Granular permissions implement least privilege — each account has exactly the permissions it needs, no more. A compromised account can only do what that account was supposed to do. The damage is bounded.
Forgetting About Indirect Access
Authorization checks protect your primary endpoints. But data often flows through secondary paths: exports, search results, aggregate reports, audit logs, error messages. Each of these is a potential read path for restricted data.
A user who can't access /api/hr/salaries might still be able to see salary data through a search endpoint that returns full documents, or through a report that aggregates data across employees they have no permission to see.
Consequences: Attackers probe non-obvious data paths, not just the primary routes. An export function that dumps data beyond what the user is authorized to see is a full data breach in a different wrapper. Search functionality that returns results from restricted resources exposes data without technically "accessing" the protected endpoint.
Fix: Apply authorization checks to every data access path, not just CRUD endpoints. When implementing exports, reports, search, or any feature that reads data, verify that the user is authorized to see each piece of data — not just that they're authorized to use the feature. Reuse the same authorization logic that protects your primary endpoints.
Why it works: Consistent authorization logic applied to all data access paths closes the gaps that selective enforcement leaves open. If the same check runs regardless of how the data was requested, there's no secondary path to exploit.
Insecure Permission Changes
The process of modifying permissions is itself a sensitive operation — and often insufficiently secured.
Common issues: no audit trail for who changed what permissions and when; no approval workflow before a sensitive role is granted; users who can assign roles to themselves or others without verification; no notifications when security-critical permissions change.
Consequences: A compromised account that can change permissions becomes significantly more dangerous — it can silently escalate its own privileges or create backdoor accounts with elevated access. Without audit logs, the escalation may go undetected for months. Without alerts, security teams don't know to investigate.
Fix: Log every permission change immutably, including the actor, the target, and what changed. Implement approval workflows for sensitive role grants — require a second authorized user to confirm before the permission takes effect. Notify security stakeholders for significant changes (e.g., any grant of admin-level roles). Restrict permission assignment to a small number of highly trusted accounts.
Why it works: Audit logs make privilege escalation detectable after the fact. Approval workflows require two accounts to be compromised simultaneously — a much harder bar to clear. Notifications create the human oversight that catches anomalies automated checks might miss.
The Stale Permissions Problem
Permissions granted for a reason often outlast that reason. A contractor gets temporary admin access to complete an integration — and still has it two years later. An employee moves to a different team — their old access remains. Emergency elevated access granted during an incident — never revoked.
Consequences: Your list of active credentials quietly grows while your awareness of it shrinks. Former employees, contractors, and accounts from completed projects are live credentials with no legitimate owner. Any of them compromised gives an attacker access you forgot you'd granted.
Fix: Implement access expiration by default. Temporary elevated access should have an explicit end date; when the date passes, access is automatically revoked. Conduct regular access reviews — quarterly or after organizational changes — to audit who has what and remove what's no longer needed. Automate cleanup of inactive accounts. Track "break-glass" emergency access explicitly and require post-incident review.
Why it works: Expiration turns "remove access when the project ends" from a task someone has to remember into something that happens automatically. Regular reviews catch what automation misses. The attack surface of forgotten credentials shrinks toward zero.
Summary
Authorization vulnerabilities are common because they're easy to implement inconsistently. Authentication is often a single gatekeeping moment; authorization needs to be enforced at every action, on every resource, across every data path.
The two highest-impact practices: default to deny (nothing is permitted unless explicitly granted), and check ownership alongside role (being allowed to access a resource type doesn't mean being allowed to access every instance of it).
Related posts in this series:
- The Difference Between Authentication and Authorization — what these concepts mean and why confusing them causes real problems
- Role-Based Access Control (RBAC) for Beginners — how to structure permissions before the system grows too complex to manage
- Implementing Basic Permission Systems — technical patterns for checking permissions in code