Authentication vs. Authorization and the IDOR Vulnerability
Broken Access Control: When the Server Forgets to Check Permissions
Broken access control is the #1 risk in the OWASP Top 10 2021. It occurs when a user can act outside their intended permissions — accessing other users' data, performing privileged actions, or viewing restricted pages. The root cause is almost always the same: the server authenticates the user (verifies identity) but fails to authorize the specific action (verify permission).
Authentication vs. Authorization
These two terms are frequently confused but serve fundamentally different purposes:
Authentication (AuthN) answers: "Who are you?" It verifies the user's identity through credentials (passwords, tokens, biometrics). After authentication, the server knows which user is making the request.
Authorization (AuthZ) answers: "Are you allowed to do this?" It verifies whether the authenticated user has permission to perform the requested action on the requested resource.
A system can have strong authentication and zero authorization. For example, a login page that correctly verifies usernames and passwords but then allows any logged-in user to access any other user's data by changing an ID in the URL.
What Is IDOR?
Insecure Direct Object Reference (IDOR) is the most common manifestation of broken access control. It occurs when an application exposes a reference to an internal object (database record, file, key) and the server fails to verify that the user is authorized to access that specific object.
Vulnerable code:
@app.route('/api/orders/<order_id>')
def get_order(order_id):
user = get_current_user() # Authenticated
order = db.query(f"SELECT * FROM orders WHERE id = {order_id}")
return jsonify(order) # No authorization check!
An authenticated user can change order_id from 1001 (their own order) to 1002 (another user's order) and the server returns it.
Why IDOR Is So Prevalent
- Developers assume obscurity is security: "The IDs are sequential, but users won't guess other IDs." Attackers do guess — it is trivial.
- Client-side checks are trusted: A UI may hide admin buttons from non-admin users, but the server does not enforce the same restriction.
- Object-level checks are forgotten: Developers check authentication at the login endpoint but forget to check authorization at every object access endpoint.
- API endpoints are overlooked: REST APIs often expose object references without the same authorization logic as the web UI.
Real-World Impact
IDOR has been responsible for numerous high-profile breaches:
- Facebook (2018): An IDOR vulnerability allowed attackers to access any user's phone number by manipulating an API parameter.
- Equifax (2017): While not purely IDOR, the breach involved accessing records the attacker was not authorized to view.
- Uber (2016): An IDOR flaw allowed drivers to view other drivers' earnings and personal information.
The Principle of Complete Mediation
The security principle that prevents IDOR is complete mediation: every access to every object must be checked against the access control policy. There are no exceptions, no "trusted" internal calls, and no assumptions about how the user arrived at the request.
Implementing Server-Side Authorization
Fixing IDOR with Object-Level Authorization Checks
The fix for IDOR is conceptually simple — check authorization on every object access — but requires discipline to implement consistently across an entire application.
The Authorization Check Pattern
Every endpoint that accesses a specific object must verify that the current user is authorized to access that object:
@app.route('/api/orders/<order_id>')
def get_order(order_id):
user = get_current_user()
order = db.query(f"SELECT * FROM orders WHERE id = {order_id}")
# Authorization check — verify ownership
if order.user_id != user.id:
return jsonify({'error': 'Forbidden'}), 403
return jsonify(order)
This single check transforms a vulnerable endpoint into a secure one. The key is that the check happens on the server, every time, for every object.
Role-Based Access Control (RBAC)
For applications with distinct user roles (user, moderator, admin), implement role-based checks:
@app.route('/api/admin/users/<user_id>/delete', methods=['DELETE'])
def delete_user(user_id):
user = get_current_user()
# Role check
if user.role != 'admin':
return jsonify({'error': 'Admin access required'}), 403
# Proceed with deletion
db.delete_user(user_id)
return jsonify({'status': 'deleted'})
Attribute-Based Access Control (ABAC)
For more complex scenarios, attribute-based access control evaluates multiple attributes (user role, resource ownership, time of day, location, etc.):
def can_access_order(user, order):
# Owner can always access
if order.user_id == user.id:
return True
# Support team can access during business hours
if user.role == 'support' and is_business_hours():
return True
# Admin can always access
if user.role == 'admin':
return True
return False
Why Unpredictable IDs Are Not Enough
A common misconception is that using UUIDs or random IDs instead of sequential integers prevents IDOR. It does not:
- IDs leak through other channels: API responses, referrer headers, browser history, and logs all expose object IDs.
- IDs can be brute-forced: A UUIDv1 contains timestamp information. Even UUIDv4 can be guessed if the attacker has access to any other user's ID (they can infer the ID space).
- Security through obscurity fails: Relying on the secrecy of an identifier is not a security control. The server must enforce authorization regardless of how the ID was obtained.
Unpredictable IDs are a useful defense-in-depth measure (they make enumeration harder), but they must never replace authorization checks.
Testing for IDOR
To test for IDOR, follow this methodology:
- Create two accounts: Account A (low privilege) and Account B (different user or higher privilege).
- Identify object references: Note every ID in URLs, request parameters, and API responses.
- Swap IDs: Make requests to Account A's endpoint but replace Account A's object IDs with Account B's object IDs.
- Verify the response: If the server returns Account B's data, the vulnerability exists.
# Account A requests their own order
curl -H "Cookie: session=A" https://api.example.com/orders/1001
# Returns order 1001 (expected)
# Account A requests Account B's order
curl -H "Cookie: session=A" https://api.example.com/orders/1002
# If this returns order 1002's data, IDOR exists
Practice: Find the IDOR
Review this code and identify the vulnerability:
@app.route('/api/documents/<doc_id>')
def download_document(doc_id):
user = get_current_user()
document = Document.query.get(doc_id)
if not document:
return jsonify({'error': 'Not found'}), 404
return send_file(document.file_path)
The vulnerability: there is no check that document.owner_id == user.id. Any authenticated user can download any document by guessing or enumerating document IDs. The fix is to add an ownership check before serving the file.
Practice Problems
The following endpoint allows users to download their uploaded files. Identify the IDOR vulnerability and fix it. ```python @app.route('/files/<file_id>') def download(file_id): user = get_current_user() file = File.query.get(file_id) if not file: abort(404) return send_file(file.path) ```
Solution
The endpoint does not verify that the file belongs to the current user. Any authenticated user can download any file by changing the file_id. Fix:
```python
@app.route('/files/<file_id>')
def download(file_id):
user = get_current_user()
file = File.query.get(file_id)
if not file:
abort(404)
if file.owner_id != user.id:
abort(403)
return send_file(file.path)
```
The ownership check (`file.owner_id != user.id`) ensures users can only download their own files. Design an authorization system for a multi-tenant SaaS application where: - Users belong to organizations - Users have roles within their organization (owner, admin, member) - Resources (projects, documents) belong to organizations - Owners and admins can manage all resources in their organization - Members can only access resources explicitly shared with them
Solution
Implement a middleware-based authorization system:
```python
def authorize(resource_type, resource_id, required_permission):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
user = get_current_user()
resource = get_resource(resource_type, resource_id)
# Check organization membership
if resource.org_id != user.org_id:
abort(403)
# Check role-based permissions
if user.role in ('owner', 'admin'):
return f(*args, **kwargs)
# Check explicit sharing for members
if has_explicit_access(user.id, resource_type, resource_id):
return f(*args, **kwargs)
abort(403)
return decorated_function
return decorator
@app.route('/api/projects/<project_id>')
@authorize('project', 'project_id', 'read')
def get_project(project_id):
# ...
```
Key principles: verify organization membership first, then check role-based permissions, and finally check explicit sharing for non-admin roles. Quiz
1. What is the difference between authentication and authorization?
2. An authenticated user changes the `orderId` parameter in a URL from `orderId=1001` to `orderId=1002` and sees another user's order. What is this vulnerability called?
3. Why are unpredictable IDs (UUIDs) not a sufficient defense against IDOR?
4. Which security principle requires that every access to every object is checked against the access control policy?
5. A web application hides admin buttons from non-admin users in the UI, but the server does not enforce the same restriction. An attacker can directly call the admin API endpoint. What type of vulnerability is this?
Flashcards
Question
What is the difference between authentication and authorization?
Click to reveal answer
Answer
Authentication verifies identity ('Who are you?'). Authorization verifies permissions ('Are you allowed to do this?'). A system can have strong authentication but zero authorization.
Question
What is IDOR?
Click to reveal answer
Answer
Insecure Direct Object Reference — a vulnerability where an application exposes a reference to an internal object and fails to verify that the user is authorized to access that specific object. The most common manifestation of broken access control.
Question
Why are unpredictable IDs (UUIDs) not a sufficient defense against IDOR?
Click to reveal answer
Answer
IDs leak through API responses, headers, history, and logs. Security through obscurity is not a security control. The server must enforce authorization regardless of how the ID was obtained.
Question
What is the Complete Mediation principle?
Click to reveal answer
Answer
Every access to every object must be checked against the access control policy. No exceptions, no trusted internal calls, no assumptions about how the user arrived at the request.
Question
Why are client-side authorization checks insufficient?
Click to reveal answer
Answer
Client-side checks (hiding buttons, disabling UI elements) can be bypassed by directly calling the API endpoint. Authorization must always be enforced on the server, where the attacker cannot manipulate the request.
Revision Notes
Key Takeaways
- 1. Broken access control is the #1 risk in OWASP Top 10 2021 — the server authenticates but fails to authorize
- 2. IDOR occurs when changing an identifier in a request returns another user's data
- 3. Authorization must be enforced server-side for every object reference, not just at login
- 4. Unpredictable IDs (UUIDs) make enumeration harder but do not replace authorization checks
- 5. The Complete Mediation principle requires checking every access against the access control policy
Interview Tips
- • Explain the difference between authentication and authorization with a concrete example of a system that has one but not the other
- • Walk through how you would test for IDOR using two accounts and ID swapping
- • Discuss why unpredictable IDs are not a substitute for authorization — mention ID leakage through multiple channels
- • Connect broken access control to real-world breaches (Facebook, Uber) to show practical understanding
Cheat Sheet
Broken Access Control and IDOR Cheat Sheet
Phase: Web Vulnerabilities and the OWASP Top 10 | Level: intermediate
Key Concepts:
- Authentication = identity verification; Authorization = permission verification
- IDOR = accessing other users' objects by manipulating references
- Complete Mediation = check authorization on every object access
Common IDOR Patterns:
- URL parameters:
/api/orders/<order_id>— swap IDs to access other users' data - API endpoints:
/api/users/<user_id>/profile— no ownership check - File downloads:
/files/<file_id>— no ownership check
Defense:
- Server-side authorization check on every object access
- Role-Based Access Control (RBAC) for role-based permissions
- Attribute-Based Access Control (ABAC) for complex scenarios
- Never rely on client-side checks or ID unpredictability
Testing Method:
- Create two accounts (A and B)
- Note all object IDs in requests
- Swap Account A's IDs with Account B's IDs
- If the server returns Account B's data, IDOR exists
Key points:
- IDOR occurs when changing an identifier in a request returns another user's data
- Authorization must be enforced server-side for every object reference, not just at login
- Sequential IDs make guessing trivial, but unpredictable IDs do not replace authorization checks
- Client-side checks (hiding UI elements) are not a security control
Tags: access-control, web-security
Continue Learning
Before You Start
- No prerequisites listed.