Broken Access Control
Access control decides what an authenticated (or anonymous) user is allowed to do: which records they can read, which actions they can perform, which admin functions they can reach. When it's missing or wrong, users can view other customers' invoices by changing an ID in the URL, call admin APIs from a regular account, or read another tenant's data in a SaaS product. Broken access control has been #1 on the OWASP Top 10 since 2021, found in a large share of tested applications, and it's #1 on the OWASP API Security Top 10 as Broken Object Level Authorization (BOLA).
It's so common because authorization is application-specific logic. No framework can know that "support agents may view but not refund orders over $500 in other regions". Every endpoint and every query must enforce the rules, and a single missed check is a breach.
TL;DR
- IDOR / BOLA: accessing objects by ID without checking ownership. It's the most common and most damaging form.
- Function-level failures: missing role checks on admin or privileged endpoints ("hidden" URLs aren't protection).
- Deny by default: every route requires authentication and explicit authorization rules.
- Enforce authorization server-side, per request, per object. Never trust client-side checks or client-supplied roles.
- Scope queries by user or tenant (
WHERE tenant_id = :current_tenant), or use database row-level security. - Test authorization systematically: user A must not access user B's resources through any endpoint.
Quick Example
A classic IDOR and its fix:
Defense in depth at the database level, with PostgreSQL row-level security:
Core Concepts
Types of Access Control Failures
Authentication vs Authorization
Authentication establishes who the user is. Authorization decides what they may do. Many breaches involve fully authenticated users simply accessing what they shouldn't. Logging in isn't permission.
Authorization Models
- RBAC (role-based): permissions attach to roles (admin, editor, viewer). It's simple, but coarse.
- ABAC (attribute-based): rules over user, resource, and context attributes ("same region and amount < 500").
- ReBAC (relationship-based): access follows relationships ("editor of the folder containing this document"), as popularized by Google Zanzibar and implemented in OpenFGA, SpiceDB, and Permify.
- Ownership and tenancy checks: the most common rule, "the resource belongs to the caller's account or organization".
Most apps combine RBAC for functions with ownership and tenant checks for objects.
Where to Enforce
- Centralize policy logic (a policy module, middleware and decorators, or a policy engine like OPA, Cedar, or OpenFGA) rather than scattering ad hoc
ifstatements. - Enforce at the data access layer: repository methods that require a tenant or user context, ORM global filters, or database row-level security make it hard to forget the check.
- Enforce on every entry point: REST, GraphQL resolvers and
node(id:)fields, WebSocket messages, background jobs triggered by users, file download URLs, and exports. See GraphQL security.
Mass Assignment
Frameworks that bind request bodies directly to models let attackers set fields they shouldn't (is_admin, org_id, balance). Use explicit input schemas and allowlists of writable fields (DTOs, Pydantic models, serializer fields) rather than binding entire entities.
Best Practices
Deny by Default
Require authentication for all routes unless explicitly public, and require an explicit permission for every privileged action. New endpoints are then safe by default, instead of open by default.
Don't Rely on Obscurity
Unguessable IDs (UUIDs) reduce enumeration but aren't authorization: IDs leak through logs, referrers, shared links, and other APIs. Always check access. See distributed IDs.
Return 404 for Unauthorized Objects
For resources the user shouldn't know about, respond as if they don't exist, so attackers can't confirm valid IDs. Use 403 when the user legitimately knows the resource exists but lacks permission for the action.
Log and Monitor Authorization Failures
Repeated 403s and 404s across sequential IDs signal probing. Log denials with user, resource, and action, alert on anomalies, and rate-limit. See audit logging.
Test Authorization Like a Feature
Maintain automated tests for each role and ownership rule: user A vs user B, tenant X vs tenant Y, regular user vs admin, across every endpoint. Tools like Burp's Autorize and API security scanners help find gaps.
Common Mistakes
Checking Permissions Only in the UI
Hiding the "Delete" button for non-admins doesn't stop anyone from calling DELETE /api/projects/9 directly. Every server endpoint must check.
Trusting Client-Supplied Identity
Derive the acting user from the verified session or token, never from the request body, and verify that from_account belongs to them.
Forgetting Secondary Paths
Main endpoints are protected, but CSV exports, search APIs, GraphQL nested fields, webhooks, file URLs, and legacy v1 endpoints aren't. Inventory all access paths to sensitive data.
FAQ
What is an IDOR vulnerability?
Insecure Direct Object Reference: the application uses a user-supplied identifier (an ID in a URL, body, or header) to fetch an object without verifying that the user may access it. Changing the ID exposes or modifies other users' data. In API security terminology, it's called Broken Object Level Authorization (BOLA).
Why is broken access control the #1 OWASP risk?
It's extremely common, since every application has custom authorization logic and one missed check is enough. It's often easy to exploit (changing an ID), and its impact is severe: mass data exposure, account takeover, or privilege escalation. Automated scanners also struggle to find it, because they don't know the business rules.
Do UUIDs prevent IDOR?
They make blind enumeration impractical, but IDs still leak through URLs, logs, emails, shared links, and other API responses. Any leaked ID becomes an attack if authorization isn't checked. UUIDs are defense in depth, not a fix.
What's the best place to enforce multi-tenant isolation?
In several layers: tenant-scoped queries in the data access layer (ideally impossible to bypass accidentally), database row-level security as a safety net, and tests proving cross-tenant access fails. Also include the tenant in caches, search indexes, and file storage paths.
Related Topics
- OWASP Top 10 — The web application risk list
- API Security — BOLA and API-specific risks
- Row-Level Security — Database-enforced isolation
- Authentication — Establishing identity before authorization
- Spring Security — Method-level authorization in Java
- Security Misconfiguration — CORS and default settings