API Security: Understanding and Preventing Access Control Vulnerabilities
By Théophile Reverdell
Introduction
APIs (Application Programming Interfaces) have become the backbone of modern software architecture. They allow applications to communicate with each other, power mobile applications, and orchestrate microservices. However, this ubiquity makes APIs a prime target for attackers.
Among the most critical and widespread vulnerabilities are Broken Access Control issues, ranked #1 in the OWASP Top 10 2021 and #1 in the OWASP API Security Top 10 2023.
This article explores these vulnerabilities through a practical case, then presents best practices for securing your APIs.
Authentication vs Authorization: A Crucial Distinction
Before going further, let’s clarify two often confused concepts:
Authentication
Authentication answers the question: “Who are you?”
- Verification of user identity
- Generally via login/password, tokens, or certificates
- Result: a session or access token
Authorization
Authorization answers the question: “What are you allowed to do?”
- Verification of permissions on a resource
- Based on roles, rules, or data ownership
- Must be checked on every request
The classic pitfall: implementing authentication correctly but forgetting authorization.
Practical Case: IDOR on a REST API
The Scenario
Imagine a REST API with the following endpoints:
| Endpoint | Description |
|---|---|
POST /api/signup | Registration |
POST /api/login | Login |
GET /api/user/{user_id} | Retrieve user information |
PUT /api/note | Update private note |
The API requires authentication to access user data. Everything seems correct… apparently.
The Vulnerability
Testing the /api/user/{user_id} endpoint reveals a major problem:
# Login as user "alice" (user_id = 2)
curl -X POST /api/login -d '{"username":"alice","password":"***"}' -c cookies.txt
# Access own data - OK
curl /api/user/2 -b cookies.txt
# {"userid": 2, "username": "alice", "note": "My private note"}
# Access another user's data - PROBLEM!
curl /api/user/1 -b cookies.txt
# {"userid": 1, "username": "admin", "note": "Confidential data..."}
The API verifies that the user is authenticated, but doesn’t verify they are authorized to access the requested resource.
Vulnerability Classification
This vulnerability has several names:
- IDOR (Insecure Direct Object Reference): Insecure direct object reference
- BOLA (Broken Object Level Authorization): Broken object-level authorization
- Horizontal Privilege Escalation: Horizontal privilege escalation
Impact of IDOR Vulnerabilities
The consequences can be severe:
Confidentiality
- Access to other users’ personal data
- Leakage of sensitive information (emails, addresses, financial data)
- Intellectual property theft
Integrity
- Modification of other users’ data
- Deletion of unauthorized resources
- Transaction manipulation
Compliance
- GDPR and other regulation violations
- Potential significant fines
- Reputation damage
Detecting IDOR Vulnerabilities
Warning Signs
- Sequential IDs in URLs:
/api/user/1,/api/user/2 - IDs in request bodies:
{"user_id": 123, "action": "delete"} - Query parameters:
/api/orders?userId=456 - Custom headers:
X-User-ID: 789
Testing Methodology
# 1. Create two distinct accounts
# Account A (user_id: 10) and Account B (user_id: 11)
# 2. Login with account A
curl -X POST /api/login -d '{"username":"A","password":"***"}' -c cookies_A.txt
# 3. Attempt to access account B's resources
curl /api/user/11 -b cookies_A.txt
curl /api/orders?userId=11 -b cookies_A.txt
curl -X DELETE /api/documents/doc_B_id -b cookies_A.txt
# 4. Observe responses
# - 403 Forbidden = Properly protected
# - 200 OK with data = VULNERABLE
Remediation and Best Practices
1. Systematic Authorization Validation
Each endpoint must verify access rights:
@app.route('/api/user/<int:user_id>')
@login_required
def get_user(user_id):
current_user = get_current_user()
# Explicit authorization check
if user_id != current_user.id and not current_user.is_admin:
return jsonify({"error": "Unauthorized"}), 403
user = User.query.get_or_404(user_id)
return jsonify(user.to_dict())
2. Use Indirect References
Instead of exposing internal IDs, use:
UUIDs instead of sequential IDs:
# Before (predictable)
/api/documents/123
# After (non-predictable)
/api/documents/7f3d9a2e-8b4c-4d5f-a6e7-1c2d3e4f5a6b
Session-based references:
# Before
@app.route('/api/user/<int:user_id>')
def get_user(user_id):
return User.query.get(user_id)
# After - automatically uses logged-in user
@app.route('/api/user/me')
@login_required
def get_current_user_profile():
return User.query.get(current_user.id)
3. Implement Centralized Access Control
Create a reusable middleware or decorator:
def authorize_resource_access(resource_type):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
resource_id = kwargs.get('resource_id')
current_user = get_current_user()
if not can_access(current_user, resource_type, resource_id):
abort(403)
return f(*args, **kwargs)
return decorated_function
return decorator
@app.route('/api/documents/<resource_id>')
@login_required
@authorize_resource_access('document')
def get_document(resource_id):
return Document.query.get(resource_id)
4. Principle of Least Privilege
Apply minimal permissions by default:
# Default: DENY access
def can_access(user, resource):
# Explicit list of access conditions
if resource.owner_id == user.id:
return True
if resource.is_public:
return True
if user.has_role('admin'):
return True
# Default: deny
return False
5. Logging and Monitoring
Track unauthorized access attempts:
def log_access_attempt(user, resource, success):
logger.info(
f"Access {'granted' if success else 'DENIED'}: "
f"user={user.id}, resource={resource.id}, "
f"ip={request.remote_addr}"
)
if not success:
# Alert on suspicious behavior
if count_denied_attempts(user) > THRESHOLD:
alert_security_team(user)
Recommended Security Architecture
Protection Layers
┌─────────────────────────────────────┐
│ Rate Limiting │ ← Protection against enumeration
├─────────────────────────────────────┤
│ Authentication │ ← Who are you?
├─────────────────────────────────────┤
│ Authorization │ ← Can you access?
├─────────────────────────────────────┤
│ Input Validation │ ← Valid data?
├─────────────────────────────────────┤
│ Business Logic │ ← Request processing
└─────────────────────────────────────┘
API Security Checklist
- Authentication required on all sensitive endpoints
- Authorization check on every resource access
- Use of non-predictable identifiers (UUIDs)
- Rate limiting to prevent enumeration
- Access attempt logging
- Automated security testing (DAST/SAST)
- Code review focused on access controls
Automated Testing
Integrate security tests into your CI/CD pipeline:
def test_idor_protection():
# Create two users
user_a = create_user("userA")
user_b = create_user("userB")
# Create a resource for user_a
resource = create_resource(owner=user_a)
# Attempt to access with user_b
response = client.get(
f'/api/resources/{resource.id}',
headers=auth_headers(user_b)
)
# Should return 403, not 200
assert response.status_code == 403
def test_user_can_access_own_resource():
user = create_user("owner")
resource = create_resource(owner=user)
response = client.get(
f'/api/resources/{resource.id}',
headers=auth_headers(user)
)
assert response.status_code == 200
Security Testing Tools
Automated Tools
- OWASP ZAP: Free web vulnerability scanner
- Burp Suite: Complete security testing suite
- Postman: API testing with security collections
Manual Methodology
- Intercept requests with a proxy
- Identify identification parameters
- Modify these parameters with other users’ values
- Observe API responses
Conclusion
Access control vulnerabilities in APIs are among the most critical and widespread. They often result from confusion between authentication and authorization, or forgetting to verify access rights.
Key takeaways:
- Authentication is not enough: Always verify authorization
- Validate every request: Never trust client-side parameters
- Use indirect references: UUIDs rather than sequential IDs
- Centralize controls: A reusable authorization middleware
- Test regularly: Automated tests and manual audits
- Log access: To detect suspicious behavior
API security is an ongoing process. By integrating these practices from design and maintaining constant vigilance, you will significantly reduce the risks of compromising your data and your users’ data.
Additional resources:
- OWASP API Security Top 10
- OWASP Top 10 2021 - Broken Access Control
- OWASP Testing Guide - Authorization Testing
- PortSwigger - IDOR
This article is based on security analysis concepts practiced on the Root-Me platform, an excellent resource for learning cybersecurity in a practical way.