Bola Idor in Django with Bearer Tokens
Bola Idor in Django with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Broken Object Level Authorization (BOLA) occurs when an API lacks proper ownership and authorization checks, allowing one subject to act on another subject’s resources. In Django, combining token-based authentication (e.g., Bearer Tokens) with object-level permissions can inadvertently expose BOLA when developers authenticate the request but forget to enforce per-object ownership or tenant boundaries.
Bearer Tokens in Django are typically passed via the Authorization header as Bearer <token>. A token may identify a user or an API client, but if views resolve the token to a user and then directly fetch objects using only the object identifier (e.g., URL parameter pk) without verifying that the object belongs to or is accessible to that user, BOLA arises. For example, consider a REST endpoint like /api/profiles/{id}/. If the view uses get_object_or_404(Profile, id=id) after validating the Bearer token, any authenticated user with a valid token can access any profile by guessing or iterating IDs, because the query does not scope the lookup to the authenticated subject.
Common patterns that enable BOLA with Bearer Tokens include:
- Using token-authenticated user but querying with
.get()or.filter()without matching the user or tenant field. - Relying on object-level permissions libraries (e.g., Django Guardian) without integrating token identity into the permission check.
- Caching token-to-user mappings without re-validating scope on each request, allowing horizontal access across unrelated resources.
Real-world examples often involve endpoints that expose sensitive data (e.g., /api/users/{user_id}/settings/) where the token identifies Alice, but the view returns Bob’s settings when user_id is altered. OWASP API Top 10 categorizes this as Broken Object Level Authorization, and it maps to frameworks such as PCI-DSS and SOC2 controls around access enforcement. Attack techniques like IDOR enumeration or privilege escalation via tampered identifiers are practical risks when Bearer Tokens provide authentication but not authorization scoping.
In Django, this is not a flaw in Bearer Tokens themselves, but a design oversight: authentication (token validation) must be coupled with authorization checks that bind resources to the authenticated identity or tenant. Without explicit ownership or scope checks, the unauthenticated attack surface includes ID enumeration and unauthorized data access even when tokens are required.
Bearer Tokens-Specific Remediation in Django — concrete code fixes
Remediation centers on ensuring that every object access is scoped to the identity derived from the Bearer Token. Below are concrete, idiomatic Django examples that demonstrate secure patterns.
1. Token validation with Django REST Framework and scope-aware lookup
Use DRF’s authentication and override get_queryset to filter by the authenticated user derived from the Bearer Token. This ensures list and detail endpoints honor ownership.
from rest_framework import viewsets, permissions
from django.shortcuts import get_object_or_404
from .models import UserProfile
from .serializers import UserProfileSerializer
class UserProfileViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = UserProfileSerializer
def get_queryset(self):
# The request.user is set by DRF after Bearer token authentication
return UserProfile.objects.filter(user=self.request.user)
def retrieve(self, request, pk=None):
obj = get_object_or_404(self.get_queryset(), pk=pk)
serializer = self.get_serializer(obj)
return Response(serializer.data)
By using get_queryset to filter on user=self.request.user, the detail endpoint automatically enforces BOLA: a token belonging to Alice cannot retrieve Bob’s profile even if she guesses the numeric ID.
2. Token-based scoping with custom permission classes
Define a permission that explicitly checks ownership or tenant membership and apply it at the view or global level.
from rest_framework.permissions import BasePermission, SAFE_METHODS
from django.shortcuts import get_object_or_404
class IsOwnerOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
# Read permissions are allowed for any request,
# but write permissions require owner match
if request.method in SAFE_METHODS:
return True
return obj.user == request.user
from rest_framework import generics
from .models import UserProfile
from .serializers import UserProfileSerializer
class UserProfileDetail(generics.RetrieveUpdateAPIView):
queryset = UserProfile.objects.all()
serializer_class = UserProfileSerializer
permission_classes = [IsOwnerOrReadOnly]
def get_queryset(self):
# Ensure list endpoints are also scoped
return UserProfile.objects.filter(user=self.request.user)
This pattern separates authentication (handled by DRF’s Bearer token authentication) from authorization (handled by the permission class), making BOLA prevention explicit and testable.
3. Tenant-aware scoping for multi-tenant Bearer Token schemes
In multi-tenant setups, derive tenant from the token and scope all queries. Avoid using raw pk alone.
from rest_framework import viewsets
from .models import TenantData
class TenantDataViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = TenantDataSerializer
def get_queryset(self):
token = self.request.auth
if not token or not hasattr(token, 'tenant'):
return TenantData.objects.none()
return TenantData.objects.filter(tenant=token.tenant)
def retrieve(self, request, pk=None):
obj = get_object_or_404(self.get_queryset(), pk=pk)
return Response(self.get_serializer(obj).data)
Ensure your Bearer token model stores or can resolve a tenant (e.g., token.tenant). This prevents cross-tenant reads even when IDs are predictable.
4. Generic view safety and defensive coding
Avoid get_object_or_404(MyModel, pk=pk) without scoping. Always filter by an ownership or tenant field derived from the token. Write tests that simulate different tokens accessing non-owned resources to verify BOLA protection.
# Example test snippet (pytest + DRF)
def test_user_cannot_access_other_profiles():
client = APIClient()
token_alice = generate_token_for_user(alice)
token_bob = generate_token_for_user(bob)
client.credentials(HTTP_AUTHORIZATION=f'Bearer {token_alice}')
response = client.get('/api/profiles/bob_id/') # bob_id is not Alice’s profile
assert response.status_code == 404 # or 403, not 200
client.credentials(HTTP_AUTHORIZATION=f'Bearer {token_bob}')
response = client.get(f'/api/profiles/{bob.id}/')
assert response.status_code == 200
These examples emphasize that remediation is not about discarding Bearer Tokens, but about coupling authentication signals (the token) with strict, per-request authorization checks that scope data access to the authenticated subject’s allowed objects.
FAQ
- Does using Bearer Tokens alone prevent BOLA in Django?
No. Bearer Tokens provide authentication (identifying who or what is making the request) but do not enforce authorization. Without explicit scoping in views (e.g., filtering querysets by the authenticated user or tenant), BOLA can still occur.
- How can I verify my Django endpoints are protected against BOLA with Bearer Tokens?
Write integration tests that use different tokens to access non-owned resources and assert 403/404 responses. Combine this with code reviews that check that all object-level queries include ownership or tenant filters derived from the token.
Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |