Broken Access Control: Authorization Vulnerabilities
Authentication answers 'who are you?' Authorization answers 'what can you do?' Broken access control occurs when your application fails to properly enforce the latter, allowing users to access...
Key Insights
- Broken access control is the #1 vulnerability in the OWASP Top 10, affecting 94% of tested applications, yet it remains one of the easiest flaws to exploit through simple parameter manipulation.
- Authorization must always be enforced server-side with centralized logic—client-side checks, hidden fields, and predictable IDs provide zero security against determined attackers.
- Every endpoint needs explicit authorization verification; defaulting to “deny all” and requiring explicit grants prevents the accidental exposure that leads to most access control breaches.
What is Broken Access Control?
Authentication answers “who are you?” Authorization answers “what can you do?” Broken access control occurs when your application fails to properly enforce the latter, allowing users to access resources or perform actions beyond their intended permissions.
This vulnerability consistently holds the #1 spot on the OWASP Top 10, appearing in 94% of applications tested. The business impact is severe: unauthorized data access, account takeover, privilege escalation, and complete system compromise. The 2021 Parler data breach exposed 70TB of user data through a simple IDOR vulnerability. Facebook’s 2018 breach affected 50 million accounts due to access token mishandling.
The core problem is straightforward: developers authenticate users but forget to verify whether authenticated users should access specific resources. An authenticated user isn’t an authorized user.
Common Vulnerability Patterns
Insecure Direct Object References (IDOR)
IDOR occurs when applications expose internal object references without validating user authorization. Here’s a vulnerable endpoint:
// VULNERABLE: No authorization check
app.get('/api/users/:userId/profile', async (req, res) => {
const user = await User.findById(req.params.userId);
res.json({
email: user.email,
ssn: user.ssn,
salary: user.salary,
address: user.address
});
});
Any authenticated user can access any other user’s sensitive data by changing the userId parameter. The application trusts the client-supplied ID without verification.
Privilege Escalation
Horizontal escalation lets users access other users’ resources at the same privilege level. Vertical escalation lets users gain higher privileges entirely.
// VULNERABLE: Client-side role check
app.post('/api/admin/delete-user', async (req, res) => {
// Trusting a flag sent from the client
if (req.body.isAdmin) {
await User.deleteOne({ _id: req.body.targetUserId });
return res.json({ success: true });
}
res.status(403).json({ error: 'Forbidden' });
});
An attacker simply adds "isAdmin": true to their request body. Never trust client-supplied authorization data.
Missing Function-Level Access Control
Administrative endpoints often lack proper protection:
# VULNERABLE: No authorization on admin endpoint
@app.route('/admin/users', methods=['GET'])
def list_all_users():
# Assumes only admins know this URL exists
users = User.query.all()
return jsonify([u.to_dict() for u in users])
Security through obscurity isn’t security. Attackers routinely scan for common admin paths.
Metadata Manipulation
JWT tokens and hidden form fields are frequent targets:
// VULNERABLE: Trusting JWT claims without server verification
const token = jwt.decode(req.headers.authorization); // decode, not verify!
if (token.role === 'admin') {
// Grant admin access
}
Attackers can forge tokens with elevated privileges when you decode without cryptographic verification.
Real-World Attack Scenarios
IDOR Enumeration Attack
Here’s how attackers exploit predictable IDs:
import requests
base_url = "https://vulnerable-app.com/api/orders"
session = requests.Session()
session.cookies.set('session', 'attacker_session_token')
# Enumerate order IDs to find other users' orders
for order_id in range(1000, 2000):
response = session.get(f"{base_url}/{order_id}")
if response.status_code == 200:
order_data = response.json()
print(f"Found order {order_id}: {order_data['customer_email']}")
print(f" Items: {order_data['items']}")
print(f" Address: {order_data['shipping_address']}")
This script systematically harvests order data belonging to other customers. With sequential IDs, attackers can enumerate entire databases.
Privilege Escalation via Parameter Tampering
# Original request as regular user
curl -X PUT https://api.example.com/users/profile \
-H "Authorization: Bearer user_token" \
-H "Content-Type: application/json" \
-d '{"name": "John Doe", "email": "john@example.com"}'
# Attacker adds role parameter
curl -X PUT https://api.example.com/users/profile \
-H "Authorization: Bearer user_token" \
-H "Content-Type: application/json" \
-d '{"name": "John Doe", "email": "john@example.com", "role": "admin"}'
If the backend blindly accepts and applies all submitted fields, the attacker just granted themselves admin privileges.
Forced Browsing
# Discovering admin endpoints through common paths
curl -I https://target.com/admin
curl -I https://target.com/admin/dashboard
curl -I https://target.com/api/admin/users
curl -I https://target.com/management/console
curl -I https://target.com/actuator/env # Spring Boot actuator
Automated tools like DirBuster or ffuf can test thousands of paths in minutes.
Secure Authorization Implementation
Centralized Authorization Middleware
// authorization.middleware.js
const authorize = (allowedRoles) => {
return async (req, res, next) => {
// 1. Verify user is authenticated
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
// 2. Fetch fresh user data from database (don't trust cached roles)
const user = await User.findById(req.user.id).select('role status');
if (!user || user.status !== 'active') {
return res.status(401).json({ error: 'Invalid user' });
}
// 3. Check role authorization
if (!allowedRoles.includes(user.role)) {
// Log the attempt for security monitoring
logger.warn('Authorization failure', {
userId: req.user.id,
requiredRoles: allowedRoles,
userRole: user.role,
endpoint: req.originalUrl
});
return res.status(403).json({ error: 'Insufficient permissions' });
}
req.authorizedUser = user;
next();
};
};
// Resource ownership verification
const authorizeOwnership = (resourceFetcher) => {
return async (req, res, next) => {
const resource = await resourceFetcher(req);
if (!resource) {
return res.status(404).json({ error: 'Resource not found' });
}
// Check ownership OR admin override
if (resource.ownerId !== req.user.id && req.authorizedUser.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
req.resource = resource;
next();
};
};
// Usage
app.get('/api/orders/:orderId',
authenticate,
authorize(['user', 'admin']),
authorizeOwnership((req) => Order.findById(req.params.orderId)),
(req, res) => {
res.json(req.resource);
}
);
Python Decorator Pattern
from functools import wraps
from flask import request, g, jsonify
def require_roles(*allowed_roles):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not g.current_user:
return jsonify({'error': 'Authentication required'}), 401
# Fetch fresh role from database
user = User.query.get(g.current_user.id)
if not user or user.role not in allowed_roles:
return jsonify({'error': 'Insufficient permissions'}), 403
return f(*args, **kwargs)
return decorated_function
return decorator
def require_ownership(resource_class, id_param='id'):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
resource_id = kwargs.get(id_param) or request.view_args.get(id_param)
resource = resource_class.query.get(resource_id)
if not resource:
return jsonify({'error': 'Not found'}), 404
if resource.owner_id != g.current_user.id:
return jsonify({'error': 'Access denied'}), 403
g.resource = resource
return f(*args, **kwargs)
return decorated_function
return decorator
# Usage
@app.route('/api/documents/<int:id>', methods=['DELETE'])
@require_roles('user', 'admin')
@require_ownership(Document)
def delete_document(id):
g.resource.delete()
return jsonify({'success': True})
Testing and Detection Strategies
Unit Testing Authorization Logic
import pytest
from app import create_app
from models import User, Order
class TestOrderAuthorization:
@pytest.fixture
def app(self):
return create_app('testing')
@pytest.fixture
def user_a(self, app):
return User.create(email='a@test.com', role='user')
@pytest.fixture
def user_b(self, app):
return User.create(email='b@test.com', role='user')
@pytest.fixture
def admin(self, app):
return User.create(email='admin@test.com', role='admin')
def test_user_cannot_access_others_order(self, client, user_a, user_b):
order = Order.create(owner=user_a, total=100)
# User B tries to access User A's order
response = client.get(
f'/api/orders/{order.id}',
headers={'Authorization': f'Bearer {user_b.token}'}
)
assert response.status_code == 403
def test_admin_can_access_any_order(self, client, user_a, admin):
order = Order.create(owner=user_a, total=100)
response = client.get(
f'/api/orders/{order.id}',
headers={'Authorization': f'Bearer {admin.token}'}
)
assert response.status_code == 200
def test_unauthenticated_request_rejected(self, client, user_a):
order = Order.create(owner=user_a, total=100)
response = client.get(f'/api/orders/{order.id}')
assert response.status_code == 401
Security Test Matrix
| Endpoint | Anonymous | User (Own) | User (Other) | Admin |
|---|---|---|---|---|
| GET /orders/:id | 401 | 200 | 403 | 200 |
| PUT /orders/:id | 401 | 200 | 403 | 200 |
| DELETE /orders/:id | 401 | 200 | 403 | 200 |
| GET /admin/users | 401 | 403 | 403 | 200 |
Automate this matrix as integration tests. Every cell represents a test case.
Framework-Specific Safeguards
Django Permission Classes
from rest_framework import permissions
class IsOwnerOrAdmin(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.user.is_staff:
return True
return obj.owner == request.user
class OrderViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticated, IsOwnerOrAdmin]
def get_queryset(self):
# Query scoping: users only see their own orders
if self.request.user.is_staff:
return Order.objects.all()
return Order.objects.filter(owner=self.request.user)
Spring Security Method-Level Authorization
@RestController
@RequestMapping("/api/documents")
public class DocumentController {
@PreAuthorize("hasRole('USER')")
@GetMapping("/{id}")
public Document getDocument(@PathVariable Long id) {
Document doc = documentService.findById(id);
// Programmatic ownership check
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (!doc.getOwnerId().equals(auth.getName()) &&
!auth.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN"))) {
throw new AccessDeniedException("Not authorized");
}
return doc;
}
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/admin/purge")
public void purgeAllDocuments() {
documentService.purgeAll();
}
}
Summary and Checklist
Authorization Checklist:
- Deny by default—require explicit grants for all resources
- Enforce authorization server-side on every request
- Use centralized authorization logic, not scattered checks
- Verify resource ownership, not just authentication
- Use unpredictable identifiers (UUIDs) for sensitive resources
- Scope database queries to authorized resources
- Log authorization failures for security monitoring
- Test authorization with automated security test matrices
- Re-validate permissions on sensitive operations
- Never trust client-supplied role or permission data
Further Reading:
- OWASP Broken Access Control: https://owasp.org/Top10/A01_2021-Broken_Access_Control/
- OWASP Authorization Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html
- OWASP ASVS Access Control Requirements: https://owasp.org/www-project-application-security-verification-standard/
Broken access control vulnerabilities persist because authorization is harder than authentication. Authentication is binary—you’re logged in or you’re not. Authorization is contextual, resource-specific, and easy to overlook. Build your defenses with centralized, tested, server-side enforcement, and assume every endpoint is a target.