Why it's broken
- Authentication is centralised. Authorisation is distributed across every endpoint.
- Frameworks give auth middleware. Authorisation is business logic - you build it.
IDOR
Insecure direct object reference. App uses user-controlled ID without checking ownership.
GET /api/orders/4821
Does the user own order 4821? If not checked, any authenticated user can access any order.
Fix - verify ownership:
order = Order.objects.get(id=order_id)
if order.user_id != request.user.id:
raise PermissionDenied()
Better - scope queries:
order = Order.objects.get(id=order_id, user_id=request.user.id)
Access control models
RBAC - roles have permissions. Simple apps, fixed roles.
ABAC - attributes of user, resource, environment. Complex enterprise.
ReBAC - relationship-based. "This user is an editor of this document." Collaboration, sharing.
| Scenario | Model |
|---|---|
| Simple app | RBAC |
| Multi-tenant SaaS | RBAC per tenant or ReBAC |
| Complex enterprise | ABAC |
| Collaboration | ReBAC |
Common mistakes
Checking roles instead of permissions:
# Fragile
if user.role == "admin":
delete_user(target_id)
# Better
if user.has_permission("users:delete"):
delete_user(target_id)
Client-side only: Hiding a button isn't access control. Server must check.
Inconsistent enforcement: GET checks ownership, PUT doesn't. Attacker modifies what they can't view.
Mass assignment: API accepts { "role": "admin" } without filtering fields.
Hidden admin URLs: /api/admin/users lacks auth because "only admins know the URL."
Doing it right
Centralise policy: Define rules in one place. Auditable, testable.
Deny by default: New endpoint without auth check fails closed.
Test explicitly:
def test_user_cannot_access_other_users_order():
order = create_order(user=user_a)
response = client.get(f"/api/orders/{order.id}", auth=user_b)
assert response.status_code == 404
Log denials: Sequential ID attempts show probing.
The takeaway
Check ownership on every resource access. Permissions not role names. Server-side. Deny by default. Scope queries to authenticated user.
