Broken Access Control is the #1 category in the OWASP Top 10 2021, found in 94% of applications tested in the dataset. It covers a wide range of vulnerabilities — any situation where users can act outside their intended permissions. The consequences range from reading other users' data to full administrative takeover.
What Is Access Control?
Access control (authorization) is the enforcement of who can do what. Authentication answers "who are you?" Access control answers "what are you allowed to do?" Broken access control means those rules are not enforced — or not enforced consistently.
Access control decisions must be made server-side on every request. Client-side controls (hiding a button, not showing a link) are not access controls — they are UI convenience features. Any determined user can send arbitrary HTTP requests regardless of what the browser renders.
Insecure Direct Object References (IDOR)
IDOR is the most common broken access control pattern. The application exposes a direct reference to an internal object (database row ID, file path, account number) and does not verify that the requesting user has permission to access that specific object.
GET /api/invoices/4821 → Your invoice
GET /api/invoices/4822 → Another user's invoice (no authorization check)
GET /download?file=report_4821.pdf → Your file
GET /download?file=report_4822.pdf → Another user's file
The fix is always the same: before returning any object, verify that the requesting user is authorized for that specific object — not just that they are authenticated.
# Vulnerable
invoice = Invoice.query.get(invoice_id)
return jsonify(invoice.to_dict())
# Secure
invoice = Invoice.query.filter_by(id=invoice_id, user_id=current_user.id).first_or_404()
return jsonify(invoice.to_dict())
Vertical Privilege Escalation
A lower-privileged user accessing functions reserved for higher-privileged roles. For example, a regular user accessing admin endpoints:
POST /api/admin/users/delete (no role check — any authenticated user can call it)
GET /admin/dashboard (no authorization, just authentication)
Every administrative function must verify that the current user holds the required role, not just that they are logged in.
Horizontal Privilege Escalation
Accessing another user's data at the same privilege level. IDOR is a specific form of this. Examples: changing your own profile by modifying a hidden user_id field in a form, or accessing another user's messages by guessing a conversation ID.
Path Traversal
If file paths are constructed from user input, attackers can escape the intended directory using ../ sequences:
GET /files?name=report.pdf → /var/app/files/report.pdf
GET /files?name=../../../../etc/passwd → /etc/passwd
Never construct file paths directly from user input. Validate that the resolved, canonical path starts with the expected base directory:
import os
BASE_DIR = '/var/app/files'
filename = request.args.get('name')
full_path = os.path.realpath(os.path.join(BASE_DIR, filename))
if not full_path.startswith(BASE_DIR + os.sep):
abort(403) # Path traversal attempt
Directory Listing
Web servers that serve directory listings expose the full contents of directories to anyone who visits them. This can leak source code, configuration files, backup files, and sensitive documents.
Fix (Nginx):
autoindex off;
Fix (Apache):
Options -Indexes
Missing Function-Level Access Control
APIs often expose functionality that the UI does not link to, but that is still accessible if you know the URL. Security through obscurity is not access control. Every endpoint must enforce authorization regardless of whether it is "visible" in the interface.
A common pattern: the same API endpoint behaves differently based on a role or admin parameter that the client is expected to send. Never trust role information from the client. Look it up from the authenticated session server-side.
Access Control Design Principles
- Default deny: Start by denying all access. Explicitly grant what is needed.
- Server-side enforcement: Every authorization check must happen on the server, not in client-side code.
- Ownership verification: When accessing a resource by ID, always join on the owner/user relationship.
- Use indirect references: Instead of exposing database IDs, use opaque tokens that cannot be guessed or enumerated.
- Log access control failures: A burst of 403 responses from one IP is often a sign of IDOR probing.
Testing for Broken Access Control
The most effective approach: create two test accounts and verify that account A cannot access account B's resources. Go through every API endpoint systematically. Look for numeric IDs in URLs, query parameters, request bodies, and cookies. Try incrementing, decrementing, and substituting them.
Automated scanners can detect exposed sensitive paths, directory listing, and open redirects. Full IDOR testing requires understanding the application's data model and is best done as part of a structured penetration test.