Insecure Direct Object Reference in Django with Basic Auth
Insecure Direct Object Reference in Django with Basic Auth — how this specific combination creates or exposes the vulnerability
An Insecure Direct Object Reference (BOLA/IDOR) occurs when an API exposes a reference to an internal object—such as a numeric ID or UUID—and allows an unauthenticated or insufficiently authorized actor to manipulate that reference to access or modify data they should not see. In Django, this commonly arises when a view uses a URL parameter (e.g., /api/users/123/) to look up a model instance without verifying that the requesting user has permission to view that instance. When Basic Auth is used, the risk profile changes in subtle but important ways.
Basic Auth sends credentials in each request as a base64-encoded string that is easily decoded. While it provides transport-layer identification, it does not inherently enforce object-level authorization. A developer might assume that because Basic Auth requires a username and password, the authenticated user’s identity is sufficient to protect all resources. This assumption is incorrect when authorization checks are missing or incomplete. For example, consider a view that retrieves a user’s profile using a URL parameter:
def user_profile(request, user_id):
profile = UserProfile.objects.get(id=user_id)
return JsonProfileSerializer(profile).data
Here, user_id is taken directly from the URL with no check that the authenticated user is allowed to view that specific profile. If the endpoint is protected only by Basic Auth, any authenticated user can change user_id to access other users’ profiles. This is a classic IDOR. In a black-box scan, middleBrick tests unauthenticated paths and authenticated paths with different object references to detect whether authorization is enforced consistently across object IDs.
Another scenario involves nested resources, such as a project management API where projects contain tasks. A developer might write:
def task_detail(request, project_id, task_id):
project = Project.objects.get(id=project_id)
task = Task.objects.get(id=task_id)
return JsonTaskSerializer(task).data
Even if the developer checks that the project exists, they might omit a check that the authenticated user belongs to that project. With only Basic Auth enforcing identity, an attacker can iterate over project IDs and task IDs to enumerate tasks they should not see. middleBrick’s BOLA/IDOR checks include testing predictable IDs, missing ownership validation, and inconsistent permission logic across endpoints.
Because Basic Auth does not embed scopes or roles in the token (unlike OAuth2), developers must implement explicit object ownership checks in every view or serializer. Failing to do so means that the combination of authenticated identity (via Basic Auth) and direct object references creates a straight-line path to unauthorized data access.
Basic Auth-Specific Remediation in Django — concrete code fixes
Remediation focuses on ensuring that every object reference is validated against the authenticated user’s permissions. Below are concrete, idiomatic Django examples that demonstrate secure patterns.
1. Enforce ownership with get_object_or_404 and filtering
Instead of fetching an object by ID alone, filter by both ID and a user-related field such as owner or user.
from django.shortcuts import get_object_or_404
def user_profile(request, user_id):
profile = get_object_or_404(UserProfile, id=user_id, user=request.user)
return JsonProfileSerializer(profile).data
This ensures that if the user does not own the profile, a 404 is returned rather than exposing the existence of another user’s resource.
2. Use Django REST Framework with explicit permission classes
When using Django REST Framework (DRF), define a custom permission that checks object ownership, and apply it consistently.
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.user == request.user
from rest_framework import serializers, viewsets
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = ['id', 'user', 'email', 'bio']
class UserProfileViewSet(viewsets.ModelViewSet):
queryset = UserProfile.objects.all()
serializer_class = UserProfileSerializer
permission_classes = [IsOwnerOrReadOnly]
The permission class ensures that users can only modify their own profiles. For nested resources, extend the check to parent ownership:
class IsTaskAccessible(permissions.BasePermission):
def has_object_permission(self, request, view, task):
if request.method in permissions.SAFE_METHODS:
return True
return task.project.users.filter(id=request.user.id).exists()
3. Use Django’s built-in decorators for function-based views
For function-based views, use login_required and additional filtering:
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404
@login_required
def task_detail(request, project_id, task_id):
task = get_object_or_404(Task, id=task_id, project__users=request.user)
return JsonTaskSerializer(task).data
The key is to include a relationship check (e.g., project__users=request.user) that ties the requested object to the authenticated user. This pattern works regardless of whether you use Basic Auth or other authentication mechanisms, but it is especially important when relying on Basic Auth because identity alone is insufficient for authorization.
middleBrick’s checks validate that such ownership or scope-based filters are present and effective across all object references, helping you catch missing constraints before attackers do.
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 |