# Authorization Security Reference ## Overview Authorization verifies that a requested action or service is approved for a specific entity—distinct from authentication, which verifies identity. A user who has been authenticated is often not authorized to access every resource and perform every action. ## Core Principles ### 1. Deny by Default Every permission must be explicitly granted. The default position is denial. ```python # VULNERABLE: Implicit allow def get_document(request, doc_id): return Document.objects.get(id=doc_id) # SAFE: Explicit authorization def get_document(request, doc_id): doc = Document.objects.get(id=doc_id) if not request.user.has_permission('read', doc): raise PermissionDenied() return doc ``` ### 2. Enforce Least Privilege Assign users only the minimum necessary permissions for their role. ```python # Define minimal permission sets ROLE_PERMISSIONS = { 'viewer': ['read'], 'editor': ['read', 'write'], 'admin': ['read', 'write', 'delete', 'admin'] } ``` ### 3. Validate Permissions on Every Request Never rely on UI hiding or client-side checks alone. ```python # VULNERABLE: Authorization only on some endpoints @app.route('/api/admin/users', methods=['GET']) @require_admin # Good def list_users(): pass @app.route('/api/admin/users/', methods=['DELETE']) def delete_user(id): # Missing authorization check! User.delete(id) # SAFE: Consistent authorization @app.route('/api/admin/users/', methods=['DELETE']) @require_admin # Always check def delete_user(id): User.delete(id) ``` --- ## Insecure Direct Object References (IDOR) ### The Vulnerability IDOR occurs when attackers access or modify objects by manipulating identifiers. ```python # VULNERABLE: No ownership validation @app.route('/api/orders/') def get_order(order_id): return Order.query.get(order_id).to_dict() # Attack: User A accesses /api/orders/123 (User B's order) ``` ### Prevention **1. Validate Object Ownership** ```python # SAFE: Scope queries to current user @app.route('/api/orders/') def get_order(order_id): order = Order.query.filter_by( id=order_id, user_id=current_user.id # Ownership check ).first_or_404() return order.to_dict() ``` **2. Use Indirect References** ```python # Map user-specific indices to actual IDs def get_user_order_map(user_id): orders = Order.query.filter_by(user_id=user_id).all() return {i: order.id for i, order in enumerate(orders)} @app.route('/api/orders/') def get_order(index): order_map = get_user_order_map(current_user.id) real_id = order_map.get(index) if not real_id: raise NotFound() return Order.query.get(real_id).to_dict() ``` **3. Perform Object-Level Checks** ```python # Check permission on the specific object, not just object type def check_permission(user, action, resource): # Bad: Type-level check only # if user.can('read', 'Order'): return True # Good: Object-level check if resource.owner_id == user.id: return True if resource.organization_id in user.organization_ids: return user.has_org_permission(action, resource.organization_id) return False ``` --- ## Access Control Models ### Role-Based Access Control (RBAC) Simple but limited. Good for straightforward permission structures. ```python ROLES = { 'admin': {'create', 'read', 'update', 'delete'}, 'editor': {'create', 'read', 'update'}, 'viewer': {'read'} } def has_permission(user, action): return action in ROLES.get(user.role, set()) ``` ### Attribute-Based Access Control (ABAC) More flexible. Supports complex policies with multiple attributes. ```python def evaluate_policy(subject, action, resource, environment): """ Subject: user attributes (role, department, clearance) Action: what they're trying to do Resource: object attributes (owner, classification, type) Environment: context (time, location, device) """ # Example: Only managers can approve during business hours if action == 'approve': return ( subject.role == 'manager' and resource.department == subject.department and environment.is_business_hours ) return False ``` ### Relationship-Based Access Control (ReBAC) Access based on relationships between entities. ```python # User can view document if: # - They own it # - They're in a group that has access # - They're in the same organization def can_view(user, document): if document.owner_id == user.id: return True if user.groups.intersection(document.shared_with_groups): return True if document.org_id == user.org_id and document.org_visible: return True return False ``` --- ## Common Vulnerabilities ### Horizontal Privilege Escalation Accessing resources belonging to other users at the same privilege level. ```python # VULNERABLE: User A can access User B's profile @app.route('/api/profile/') def get_profile(user_id): return User.query.get(user_id).profile # SAFE: Only access own profile @app.route('/api/profile') def get_profile(): return current_user.profile ``` ### Vertical Privilege Escalation Accessing higher-privilege functionality. ```python # VULNERABLE: Hidden admin endpoint @app.route('/api/admin/delete-all') def delete_all(): # No authorization check Database.delete_all() # SAFE: Explicit admin check @app.route('/api/admin/delete-all') @require_role('super_admin') def delete_all(): Database.delete_all() ``` ### Path Traversal in Authorization ```python # VULNERABLE: Path-based authorization bypass @app.route('/files/') def get_file(filepath): # Attacker: /files/../../../etc/passwd return send_file(filepath) # SAFE: Validate and sanitize path @app.route('/files/') def get_file(filepath): base_dir = '/app/user_files' full_path = os.path.realpath(os.path.join(base_dir, filepath)) if not full_path.startswith(base_dir): raise PermissionDenied() return send_file(full_path) ``` ### Mass Assignment ```python # VULNERABLE: User can set admin flag @app.route('/api/users/', methods=['PATCH']) def update_user(id): user = User.query.get(id) user.update(**request.json) # Includes is_admin! # SAFE: Allowlist fields @app.route('/api/users/', methods=['PATCH']) def update_user(id): ALLOWED_FIELDS = {'name', 'email', 'bio'} user = User.query.get(id) data = {k: v for k, v in request.json.items() if k in ALLOWED_FIELDS} user.update(**data) ``` --- ## Implementation Patterns ### Middleware/Filter Pattern ```python # Apply authorization consistently via middleware class AuthorizationMiddleware: def process_request(self, request): if not self.is_authorized(request): raise PermissionDenied() def is_authorized(self, request): # Extract resource and action from request resource = self.get_resource(request) action = self.get_action(request) return request.user.has_permission(action, resource) ``` ### Policy Objects ```python class DocumentPolicy: def __init__(self, user, document): self.user = user self.document = document def can_view(self): return ( self.document.is_public or self.document.owner_id == self.user.id or self.user.is_admin ) def can_edit(self): return self.document.owner_id == self.user.id def can_delete(self): return self.document.owner_id == self.user.id or self.user.is_admin # Usage policy = DocumentPolicy(current_user, document) if not policy.can_view(): raise PermissionDenied() ``` --- ## Grep Patterns for Detection ```bash # Missing authorization checks grep -rn "def get_\|def post_\|def put_\|def delete_" --include="*.py" | grep -v "@require\|@login\|permission" # Direct object access without ownership check grep -rn "\.get(.*id)\|\.filter(id=" --include="*.py" | grep -v "user_id\|owner" # Mass assignment grep -rn "\*\*request\.\|update(\*\*\|create(\*\*" --include="*.py" # Path traversal risk grep -rn "os\.path\.join.*request\|open(.*request" --include="*.py" # Admin endpoints grep -rn "admin\|superuser" --include="*.py" | grep "route\|endpoint" ``` --- ## Authorization Testing ### Test Cases 1. **Horizontal access**: Can User A access User B's resources? 2. **Vertical access**: Can regular users access admin endpoints? 3. **Missing checks**: Are all endpoints protected? 4. **Parameter tampering**: Can IDs be manipulated? 5. **Path traversal**: Can file paths escape allowed directories? 6. **Mass assignment**: Can protected fields be modified? ### Test Automation ```python def test_horizontal_access(): user_a = create_user() user_b = create_user() resource = create_resource(owner=user_a) # User B should not access User A's resource client.login(user_b) response = client.get(f'/api/resources/{resource.id}') assert response.status_code == 403 def test_idor_enumeration(): # Try sequential IDs for i in range(1, 100): response = client.get(f'/api/resources/{i}') if response.status_code == 200: # Should be denied or return 404, not 200 assert False, f"IDOR vulnerability: /api/resources/{i}" ``` --- ## References - [OWASP Authorization Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html) - [OWASP IDOR Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html) - [OWASP Access Control Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Access_Control_Cheat_Sheet.html) - [CWE-639: Authorization Bypass Through User-Controlled Key](https://cwe.mitre.org/data/definitions/639.html) - [CWE-862: Missing Authorization](https://cwe.mitre.org/data/definitions/862.html)