HIGH insecure direct object referencedjango

Insecure Direct Object Reference in Django

How Insecure Direct Object Reference Manifests in Django

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) and does not enforce sufficient authorization checks before returning or modifying that object. In Django, this commonly arises in class-based views that retrieve a model instance using a URL parameter without confirming the requesting user has permission to access it. Consider a view that looks up an Order by its primary key:

from django.shortcuts import get_object_or_404
from myapp.models import Order

def order_detail(request, order_id):
    order = get_object_or_404(Order, pk=order_id)
    return JsonResponse({'order_id': order.id, 'status': order.status})

If this view does not verify that the authenticated user is allowed to view the specific order, a user can change the numeric order_id in the URL and enumerate or access other users' orders. This pattern extends to Django REST Framework (DRF) when a viewset exposes a lookup field such as pk without scoping the queryset to the requesting user or tenant.

Django’s URL dispatcher and generic views provide convenient shortcuts that can inadvertently encourage IDOR if misused. For example, using DetailView with a URL pattern like path('object/(?P<pk>\d+)/$', ObjectDetailView.as_view(), name='object-detail') without overriding get_queryset to filter by the current user or tenant enables direct object traversal. Similarly, custom JSON endpoints built with @api_view and request.user can be vulnerable when developers omit ownership checks:

from rest_framework.decorators import api_view
from rest_framework.response import Response
from myapp.models import Document

@api_view(['GET'])
def document_detail(request, doc_id):
    doc = Document.objects.get(pk=doc_id)
    return Response({'name': doc.name, 'content': doc.content})

In the above, any authenticated (or unauthenticated, if permissions are misconfigured) user can supply a different doc_id and access documents they should not see. IDOR can also affect related objects; if an endpoint exposes nested resources such as /users/{user_id}/invoices/{invoice_id}/, failing to validate that the invoice belongs to the user is another common IDOR vector in Django applications.

Another Django-specific scenario involves mass assignment or updates where an attacker manipulates URL or body parameters to change sensitive fields they should not control. For example, an endpoint that updates a profile but uses a dictionary update without restricting fields can allow privilege escalation when combined with IDOR:

def update_profile(request, profile_id):
    profile = get_object_or_404(Profile, pk=profile_id)
    data = request.data.dict()
    for key, value in data.items():
        setattr(profile, key, value)
    profile.save()
    return JsonResponse({'status': 'updated'})

If the request can include fields such as is_admin or role, and the endpoint does not filter them, IDOR can lead to privilege escalation. In summary, IDOR in Django typically appears where object-level authorization is missing or incomplete, especially when using generic views, DRF viewsets, or manually written endpoints that expose direct object references without scoping to the requesting user or tenant context.

Django-Specific Detection

Detecting IDOR in Django involves both static analysis of views and runtime testing to confirm whether object-level authorization is consistently enforced. During scanning, middleBrick sends a series of authenticated and unauthenticated requests to endpoints that expose object identifiers, then checks whether different users can access or modify objects that do not belong to them. For example, middleBrick might request GET /orders/123 as user A and then as user B; if user B receives the same data or a 200 OK instead of 403 or 404, a potential IDOR is flagged.

To identify IDOR in your Django codebase, review views that retrieve objects via URL parameters without filtering the queryset by the current user or tenant. Look for patterns such as:

  • Use of get_object_or_404 or Model.objects.get without scoping to request.user or request.tenant.
  • DRF viewsets that define lookup_field but do not override get_queryset to filter by ownership or tenant.
  • Endpoints that accept an identifier and perform updates without validating that the object belongs to the requester.

middleBrick’s OpenAPI/Swagger analysis (supporting 2.0, 3.0, and 3.1 with full $ref resolution) can surface endpoints that accept object identifiers and help correlate them with runtime behavior. By cross-referencing spec definitions with runtime findings, middleBrick highlights endpoints where authorization checks may be missing. The LLM/AI Security module further validates whether endpoints are unintentionally exposed, ensuring that even unauthenticated LLM endpoints do not leak object references or related logic.

In practice, you can start detection by inspecting your views:

# Check whether queryset is scoped
class OrderDetailView(generics.RetrieveAPIView):
    queryset = Order.objects.all()  # Potentially unsafe
    serializer_class = OrderSerializer

# Safer pattern: scope to user
class SecureOrderDetailView(generics.RetrieveAPIView):
    serializer_class = OrderSerializer

    def get_queryset(self):
        return Order.objects.filter(user=self.request.user)

middleBrick automates this validation at scale, testing each discovered endpoint with variations of identifiers to confirm whether access controls consistently depend on object ownership or tenant context. If you use the CLI, you can run middlebrick scan <url> to receive prioritized findings with severity and remediation guidance, including how to scope querysets and apply Django’s permission decorators or DRF’s object-level permissions.

Django-Specific Remediation

Remediating IDOR in Django centers on enforcing object-level authorization for every object reference. The most reliable approach is to scope querysets to the requesting user or tenant, using Django’s built-in protections rather than manual checks. For function-based views, filter objects before retrieving them:

from django.shortcuts import get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from myapp.models import Order

@login_required
def order_detail(request, order_id):
    order = get_object_or_404(Order, pk=order_id, user=request.user)
    return JsonResponse({'order_id': order.id, 'status': order.status})

For class-based views, override get_queryset so that every lookup automatically respects ownership:

from django.views.generic import DetailView
from myapp.models import Order

class OrderDetailView(DetailView):
    model = Order
    pk_url_kwarg = 'order_id'
    context_object_name = 'order'

    def get_queryset(self):
        return Order.objects.filter(user=self.request.user)

With Django REST Framework, override get_queryset in your viewset and apply permission classes:

from rest_framework import viewsets, permissions
from myapp.models import Document
from myapp.serializers import DocumentSerializer

class DocumentViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = DocumentSerializer
    permission_classes = [permissions.IsAuthenticated]

    def get_queryset(self):
        return Document.objects.filter(owner=self.request.user)

Additionally, use Django’s built-in decorators and permission classes to enforce rules at the view level. For example, @permission_classes([permissions.IsAuthenticatedOrReadOnly]) or custom object-level permissions via django-guardian can ensure that users only act on their own objects. When updating objects, prefer PATCH with explicit field allowlists instead of iterating over all incoming keys:

from rest_framework.decorators import action
from rest_framework.response import Response

@action(detail=True, methods=['patch'])
def partial_update_safe(self, request, pk=None):
    obj = self.get_object()  # get_queryset already scoped
    allowed = {'status', 'notes'}
    for attr, value in request.data.items():
        if attr in allowed:
            setattr(obj, attr, value)
    obj.save()
    return Response({'detail': 'updated'})

By consistently scoping querysets, validating ownership on each request, and leveraging Django’s authorization utilities, you reduce the attack surface for IDOR and ensure that object references cannot be traversed to access or modify other users’ data.

Related CWEs: bolaAuthorization

CWE IDNameSeverity
CWE-250Execution with Unnecessary Privileges HIGH
CWE-639Insecure Direct Object Reference CRITICAL
CWE-732Incorrect Permission Assignment HIGH

Frequently Asked Questions

Can middleBrick prove that my API is free of IDOR?
middleBrick detects and reports potential IDOR by testing endpoints with different identifiers and authorization contexts. It provides findings and remediation guidance, but it does not certify that an API is completely free of IDOR; developers must apply the suggested fixes and validate them in their environment.
Does using Django REST Framework prevent IDOR by default?
No. DRF does not automatically enforce object-level permissions. If you do not override get_queryset to scope objects to the requesting user or apply view-level permission checks, IDOR vulnerabilities can still exist.