Insecure Direct Object Reference in Django with Bearer Tokens
Insecure Direct Object Reference in Django with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Insecure Direct Object Reference (IDOR) occurs when an API exposes a reference to an internal object (such as a database primary key or sequential identifier) without verifying that the authenticated principal is authorized to access that specific object. In Django, using Bearer Tokens for authentication can inadvertently enable IDOR when token-based identity is used for authentication but not paired with object-level authorization checks. For example, consider a REST endpoint /api/users/<user_id>/ that relies on a Bearer Token in the Authorization header to identify the caller. The token may decode to a user ID (e.g., via a JWT payload), but if the view retrieves the target user record using a URL-supplied user_id without confirming that the authenticated user’s ID matches the requested user_id, an attacker can modify the URL to access or manipulate other users’ data.
Django REST Framework (DRF) commonly uses token authentication via TokenAuthentication or custom JWT handling. Bearer Tokens simplify client requests but do not enforce object-level permissions. An attacker can perform an IDOR by iterating over numeric or predictable IDs while authenticated with a valid Bearer Token. If the underlying queryset does not filter by the authenticated user (e.g., User.objects.get(pk=user_id) instead of User.objects.get(pk=user_id, id=request.user.id)), the API returns the requested resource regardless of the token owner’s entitlements. This vulnerability is not about token validity—it’s about missing authorization checks between the authenticated identity and the referenced object.
Real-world examples include endpoints that expose sensitive information such as /api/invoices/123/, /api/profiles/456/, or admin-only routes like /api/admin/users/789/. If the Bearer Token is valid but the view does not enforce that the authenticated subject owns or is permitted to operate on the referenced object, the scan will flag a BOLA/IDOR finding. This is a common root cause in applications that implement coarse-grained authentication (token-level) but neglect fine-grained authorization (object-level). The risk is compounded when IDs are predictable, and no additional checks such as ownership validation or permission decorators are applied.
In terms of detection, middleBrick’s BOLA/IDOR checks validate that each object reference is scoped to the authenticated subject. It confirms that views filter querysets by the authenticated user’s identity and that permission classes enforce object-level constraints. Without these safeguards, even well-formed Bearer Token implementations remain vulnerable to horizontal or vertical privilege escalation through predictable object references.
Bearer Tokens-Specific Remediation in Django — concrete code fixes
To remediate IDOR when using Bearer Tokens in Django, enforce object-level authorization by ensuring that every object reference is validated against the authenticated principal. Below are concrete code examples demonstrating secure patterns.
1. Use Django’s built-in permission mixins with object ownership
In class-based views, combine IsAuthenticated with a custom permission that checks object ownership. For example:
from rest_framework.permissions import IsAuthenticated, BasePermission
from rest_framework import viewsets
from .models import UserProfile
from .serializers import UserProfileSerializer
class IsOwnerOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
# Allow read-only permissions for any request,
# but write permissions only to the object owner.
if request.method in ('GET', 'OPTIONS', 'HEAD'):
return True
return obj.user == request.user
class UserProfileViewSet(viewsets.ModelViewSet):
queryset = UserProfile.objects.all()
serializer_class = UserProfileSerializer
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
def get_queryset(self):
# Ensure list endpoints also respect ownership if needed
return UserProfile.objects.filter(user=self.request.user)
2. Explicitly filter querysets by authenticated user in function-based views
For function-based views, always filter by the authenticated user’s ID derived from the Bearer Token. Assuming the token payload contains or maps to a user ID:
from django.http import JsonResponse
from django.contrib.auth import get_user_model
from rest_framework.authentication import get_authorization_header
from rest_framework.exceptions import PermissionDenied
User = get_user_model()
def user_detail(request, user_id):
auth_header = request.META.get('HTTP_AUTHORIZATION')
if not auth_header or not auth_header.startswith('Bearer '):
raise PermissionDenied('Authorization token required.')
token = auth_header.split(' ')[1]
# Assume decode_token returns a dict with 'user_id'
payload = decode_token(token) # implement securely
authenticated_user_id = payload.get('user_id')
if str(authenticated_user_id) != str(request.user.id):
raise PermissionDenied('You can only access your own resource.')
try:
user = User.objects.get(pk=user_id, id=request.user.id)
except User.DoesNotExist:
raise PermissionDenied('Resource not found or access denied.')
# serialize and return response
3. Apply scope-based filtering in DRF viewsets using the authenticated user
For endpoints where IDs are not owned but require tenant or scope isolation, filter by the authenticated subject:
from rest_framework import generics
from .models import Document
from .serializers import DocumentSerializer
from rest_framework.permissions import IsAuthenticated
class DocumentList(generics.ListAPIView):
serializer_class = DocumentSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
# Ensure documents are scoped to the authenticated user
return Document.objects.filter(owner=self.request.user)
4. Validate IDs against the authenticated subject in serializers
Add validation logic in serializer validate methods to reject mismatches:
from rest_framework import serializers
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = ['id', 'user', 'data']
def validate(self, attrs):
request = self.context.get('request')
if request and hasattr(request, 'user'):
if attrs.get('user') != request.user:
raise serializers.ValidationError('You can only modify your own profile.')
return attrs
These patterns ensure that Bearer Token authentication is complemented with object-level checks, preventing IDOR regardless of token validity. Always derive the subject from the token or session and filter querysets accordingly before exposing any object reference.
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 |