Broken Access Control - Prevention
Table of Contents
- Core Prevention Principles
- Secure Coding Patterns
- Input Validation Strategies
- Security Headers and Configuration
- Framework-Specific Mitigations
- Security Checklist
- Code Review Guidelines
Core Prevention Principles
1. Deny by Default
Principle: Start with no access. Explicitly grant permissions.``python
✅ GOOD: Deny by default
def get_document(doc_id):
# Assume no access
if not current_user.is_authenticated:
abort(401) # Unauthorized
doc = Document.query.get(doc_id)
if not doc:
abort(404)
# Explicitly check ownership
if doc.owner_id != current_user.id and not current_user.is_admin:
abort(403) # Forbidden
return doc
❌ BAD: Allow by default, deny exceptions
def get_document(doc_id):
doc = Document.query.get(doc_id)
if some_condition: # Incomplete checks
abort(403)
return doc # Accessible otherwise
`
2. Never Trust Client Input
Principle: All authorization decisions must be server-side.
`python
❌ BAD: Trust role from client
@app.route('/admin/users')
def admin_users():
user_role = request.form.get('role') # From client!
if user_role == 'admin':
return render_template('admin_users.html')
✅ GOOD: Check role on server
@app.route('/admin/users')
def admin_users():
if not current_user.is_authenticated:
abort(401)
if not current_user.has_role('admin'): # From server database
abort(403)
return render_template('admin_users.html')
`
3. Enforce Ownership Checks
Principle: Verify resource ownership before granting access.
`python
✅ GOOD: Verify ownership
@app.route('/order/')
def view_order(order_id):
order = Order.query.filter_by(
id=order_id,
user_id=current_user.id # Enforce ownership
).first_or_404()
return render_template('order.html', order=order)
❌ BAD: No ownership check
@app.route('/order/')
def view_order(order_id):
order = Order.query.get_or_404(order_id) # Any order!
return render_template('order.html', order=order)
`
4. Centralize Access Control
Principle: Use a single, well-tested mechanism for all authorization.
`python
✅ GOOD: Centralized access control
from functools import wraps
def require_permission(permission):
def decorator(f):
@wraps(f)
def decorated_function(args, *kwargs):
if not current_user.has_permission(permission):
abort(403)
return f(args, *kwargs)
return decorated_function
return decorator
@app.route('/admin/users')
@require_permission('admin.users.view')
def admin_users():
return render_template('admin_users.html')
❌ BAD: Scattered checks
@app.route('/admin/users')
def admin_users():
# Custom check in each function
if current_user.email not in ['admin@example.com']:
abort(403)
return render_template('admin_users.html')
`
5. Use Indirect References
Principle: Don't expose internal object IDs directly to users.
`python
✅ GOOD: Use UUIDs or mapping
import uuid
class Document(db.Model):
id = db.Column(db.Integer, primary_key=True) # Internal
public_id = db.Column(db.String(36), unique=True, default=lambda: str(uuid.uuid4()))
@app.route('/document/')
def get_document(public_id):
doc = Document.query.filter_by(
public_id=public_id,
user_id=current_user.id
).first_or_404()
return doc
❌ BAD: Sequential IDs
@app.route('/document/')
def get_document(doc_id):
doc = Document.query.get_or_404(doc_id) # Predictable!
return doc
`
Secure Coding Patterns
Pattern 1: Role-Based Access Control (RBAC)
`python
Define roles and permissions
class Role(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True)
permissions = db.relationship('Permission', secondary='role_permissions')
class Permission(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True)
resource = db.Column(db.String(80))
action = db.Column(db.String(80))
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
roles = db.relationship('Role', secondary='user_roles')
def has_permission(self, permission_name):
"""Check if user has a specific permission"""
return any(
permission.name == permission_name
for role in self.roles
for permission in role.permissions
)
Use in routes
@app.route('/api/users/', methods=['DELETE'])
def delete_user(user_id):
if not current_user.has_permission('users.delete'):
abort(403)
# Additional logic...
return {'status': 'deleted'}
`
Pattern 2: Attribute-Based Access Control (ABAC)
`python
More flexible, policy-based access control
class AccessPolicy:
@staticmethod
def can_view_document(user, document):
"""Determine if user can view document based on attributes"""
# Owner can always view
if document.owner_id == user.id:
return True
# Shared documents
if user.id in document.shared_with:
return True
# Public documents
if document.is_public:
return True
# Department members can view department docs
if document.department_id == user.department_id:
return True
return False
@app.route('/document/')
def view_document(doc_id):
doc = Document.query.get_or_404(doc_id)
if not AccessPolicy.can_view_document(current_user, doc):
abort(403)
return render_template('document.html', doc=doc)
`
Pattern 3: Resource-Level Authorization
`python
Check permissions at the resource level
class Resource:
def check_access(self, user, action):
"""Override in subclasses to implement resource-specific access control"""
raise NotImplementedError
class Document(Resource, db.Model):
id = db.Column(db.Integer, primary_key=True)
owner_id = db.Column(db.Integer, db.ForeignKey('user.id'))
def check_access(self, user, action):
"""Check if user can perform action on this document"""
if action == 'read':
return self.owner_id == user.id or self.is_public
elif action == 'write':
return self.owner_id == user.id
elif action == 'delete':
return self.owner_id == user.id or user.has_role('admin')
return False
@app.route('/document/', methods=['PUT'])
def update_document(doc_id):
doc = Document.query.get_or_404(doc_id)
if not doc.check_access(current_user, 'write'):
abort(403)
# Update logic...
return {'status': 'updated'}
`
Pattern 4: Decorator-Based Authorization
`python
Reusable decorators for common patterns
def require_ownership(resource_class, id_param='id'):
"""Decorator to ensure current user owns the resource"""
def decorator(f):
@wraps(f)
def decorated_function(args, *kwargs):
resource_id = kwargs.get(id_param)
resource = resource_class.query.get_or_404(resource_id)
if resource.owner_id != current_user.id:
abort(403)
# Pass resource to the function
kwargs['resource'] = resource
return f(args, *kwargs)
return decorated_function
return decorator
@app.route('/order/')
@require_ownership(Order, 'order_id')
def view_order(order_id, resource):
# 'resource' is the verified Order object
return render_template('order.html', order=resource)
`
Input Validation Strategies
1. Validate Resource Access
`python
✅ GOOD: Validate all aspects
def get_user_resource(resource_id):
# Validate format
try:
resource_id = int(resource_id)
except (ValueError, TypeError):
abort(400) # Bad request
# Validate existence
resource = Resource.query.get(resource_id)
if not resource:
abort(404) # Not found
# Validate ownership
if resource.owner_id != current_user.id:
abort(403) # Forbidden
return resource
`
2. Whitelist Allowed Actions
`python
✅ GOOD: Whitelist pattern
ALLOWED_ACTIONS = {'view', 'edit', 'delete', 'share'}
@app.route('/document//')
def document_action(doc_id, action):
# Validate action is allowed
if action not in ALLOWED_ACTIONS:
abort(400)
doc = Document.query.get_or_404(doc_id)
# Check permissions for this specific action
if not doc.check_access(current_user, action):
abort(403)
# Process action...
return {'status': f'{action} completed'}
`
3. Sanitize and Validate IDs
`python
✅ GOOD: Validate UUID format
import re
UUID_PATTERN = re.compile(
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
re.IGNORECASE
)
def validate_uuid(uuid_string):
if not UUID_PATTERN.match(uuid_string):
abort(400, "Invalid ID format")
return uuid_string
@app.route('/document/')
def get_document(uuid_str):
uuid_str = validate_uuid(uuid_str)
# Continue with validated UUID...
`
Security Headers and Configuration
1. HTTP Security Headers
`python
Configure security headers
@app.after_request
def set_security_headers(response):
# Prevent clickjacking
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
# Prevent MIME type sniffing
response.headers['X-Content-Type-Options'] = 'nosniff'
# Enable XSS protection
response.headers['X-XSS-Protection'] = '1; mode=block'
# Strict CORS policy
response.headers['Access-Control-Allow-Origin'] = 'https://yourdomain.com'
response.headers['Access-Control-Allow-Credentials'] = 'true'
# Content Security Policy
response.headers['Content-Security-Policy'] = "default-src 'self'"
return response
`
2. CORS Configuration
`python
✅ GOOD: Strict CORS
from flask_cors import CORS
app = Flask(__name__)
CORS(app,
origins=['https://yourdomain.com'],
allow_credentials=True,
methods=['GET', 'POST', 'PUT', 'DELETE'])
❌ BAD: Permissive CORS
CORS(app, origins='*', allow_credentials=True) # NEVER DO THIS!
`
3. Session Configuration
`python
✅ GOOD: Secure session settings
app.config['SESSION_COOKIE_SECURE'] = True # HTTPS only
app.config['SESSION_COOKIE_HTTPONLY'] = True # No JavaScript access
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # CSRF protection
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1) # Auto-logout
`
Framework-Specific Mitigations
Flask with Flask-Login
`python
from flask_login import LoginManager, login_required, current_user
login_manager = LoginManager()
login_manager.init_app(app)
Protect routes
@app.route('/profile')
@login_required # Ensures authentication
def profile():
# Still need to check authorization!
return render_template('profile.html')
Custom authorization decorator
def admin_required(f):
@wraps(f)
@login_required
def decorated_function(args, *kwargs):
if not current_user.is_admin:
abort(403)
return f(args, *kwargs)
return decorated_function
@app.route('/admin')
@admin_required
def admin_panel():
return render_template('admin.html')
`
Django
`python
Use Django's built-in decorators
from django.contrib.auth.decorators import login_required, permission_required
from django.core.exceptions import PermissionDenied
@login_required
@permission_required('app.delete_document', raise_exception=True)
def delete_document(request, document_id):
document = get_object_or_404(Document, id=document_id)
# Additional ownership check
if document.owner != request.user and not request.user.is_staff:
raise PermissionDenied
document.delete()
return redirect('documents')
`
FastAPI
`python
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
async def get_current_user(token: str = Depends(oauth2_scheme)):
user = verify_token(token)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials"
)
return user
async def require_admin(current_user: User = Depends(get_current_user)):
if not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin access required"
)
return current_user
@app.delete("/users/{user_id}")
async def delete_user(
user_id: int,
admin: User = Depends(require_admin)
):
# Delete user logic...
return {"status": "deleted"}
`
Security Checklist
Use this checklist during development and code review:
Authentication & Authorization
- [ ] All sensitive endpoints require authentication
- [ ] Authorization is checked on every request (not just page load)
- [ ] Authorization checks are server-side (never client-only)
- [ ] Default access is deny (require explicit grants)
- [ ] Role checks use centralized mechanism
- [ ] Permissions are granular enough for principle of least privilege
Resource Access
- [ ] Resource ownership is verified before access
- [ ] User can only access their own resources (unless shared)
- [ ] Resource IDs are non-sequential or use UUIDs
- [ ] Database queries include ownership filters
- [ ] File access validates user permissions
- [ ] API endpoints validate resource ownership
Input Validation
- [ ] All user input is validated and sanitized
- [ ] Resource IDs are validated for format and ownership
- [ ] Action parameters are whitelisted
- [ ] URL parameters cannot bypass access control
- [ ] POST/PUT/DELETE operations verify permissions
Session & State Management
- [ ] Sessions have appropriate timeouts
- [ ] Session tokens are cryptographically random
- [ ] Sessions invalidated on logout
- [ ] Sensitive cookies have Secure and HttpOnly flags
- [ ] CSRF protection is enabled
API Security
- [ ] All API endpoints have authentication
- [ ] API rate limiting is implemented
- [ ] API responses don't leak sensitive info
- [ ] GraphQL has query depth/complexity limits
- [ ] API documentation doesn't expose internal structure
Testing & Monitoring
- [ ] Authorization tests cover all roles
- [ ] Tests verify horizontal and vertical access control
- [ ] Authorization failures are logged
- [ ] Suspicious access patterns trigger alerts
- [ ] Regular security audits are performed
Code Review Guidelines
What to Look For
🔴 Critical Issues:
`python
Missing authorization entirely
@app.route('/admin/delete/')
def delete_user(id): # No permission check!
User.query.get(id).delete()
Client-side only authorization
if (userRole === 'admin') { // JavaScript check only!
callDeleteAPI();
}
Trusting client input
role = request.form['role'] # User controls this!
if role == 'admin':
grant_admin_access()
`
🟡 Warning Signs:
`python
Hard-coded credentials or roles
if user.email == 'admin@example.com':
Incomplete ownership checks
if user.id == document.user_id: # What about admins?
Scattered authorization logic
Different patterns used throughout codebase
`
✅ Good Patterns:
`python
Centralized authorization
@require_permission('users.delete')
def delete_user(id):
Ownership verification
doc = Document.query.filter_by(
id=doc_id,
user_id=current_user.id
).first_or_404()
Explicit role checks
if not current_user.has_role('admin'):
abort(403)
`
Review Questions
For each protected resource or function, ask:
- Who should have access? (Define explicitly)
- Is authentication required? (Verify identity)
- Is authorization checked? (Verify permissions)
- Where is the check performed? (Must be server-side)
- Can it be bypassed? (Test with different roles)
- Is ownership verified? (For user-specific resources)
- Are errors logged? (For monitoring)
Testing Strategies
Unit Tests
`python
def test_user_cannot_access_others_profile():
"""Test horizontal access control"""
user_a = create_user('user_a')
user_b = create_user('user_b')
with app.test_client() as client:
# Log in as user_a
client.post('/login', data={'username': 'user_a'})
# Try to access user_b's profile
response = client.get(f'/profile/{user_b.id}')
# Should be forbidden
assert response.status_code == 403
`
Integration Tests
`python
def test_role_based_access():
"""Test that only admins can delete users"""
admin = create_user('admin', role='admin')
regular_user = create_user('regular')
with app.test_client() as client:
# Regular user tries to delete
client.post('/login', data={'username': 'regular'})
response = client.delete('/api/users/1')
assert response.status_code == 403
# Admin can delete
client.post('/login', data={'username': 'admin'})
response = client.delete('/api/users/1')
assert response.status_code == 200
``
What's Next?
- Overview: Understand what broken access control is
- Attack Vectors: Learn how attacks happen
- Examples: See vulnerable vs secure code
- Lab: Practice fixing vulnerabilities
---
Part of the OWASP Top 10 Educational Repository