Skip to main content

Overview

Insecure Direct Object Reference (IDOR) is a vulnerability where an application provides direct access to objects based on user-supplied input. An attacker can simply change an ID in a URL (e.g., .../invoice/123 to .../invoice/124) to access data that does not belong to them. This flaw occurs because the application checks if the user is authenticated (logged in), but fails to check if they are authorized (allowed to see that specific item).

Business Impact

IDOR is a critical, high-impact vulnerability. It can lead to a complete loss of data confidentiality, allowing attackers to access and exfiltrate all user data in the system, one record at a time. They can also modify or delete other users’ data, leading to massive integrity-loss.

Reference Details

CWE ID: CWE-639 (Authorization Bypass Through User-Controlled Key) OWASP Top 10 (2021): A01:2021 - Broken Access Control Severity: High

Framework-Specific Analysis and Remediation

This is a logical flaw that frameworks cannot prevent automatically. The vulnerability is in the data-access logic. The fix is to always filter by the authenticated user’s ID in addition to the object’s ID. Never retrieve an object by its ID alone.
  • Python
  • Java
  • .NET(C#)
  • PHP
  • Node.js
  • Ruby

Framework Context

A Django view that retrieves an object using Invoice.objects.get(pk=invoice_id).

Vulnerable Scenario 1: A Document Detail View

# invoicing/views.py
from django.contrib.auth.decorators import login_required
from .models import Invoice

@login_required
def get_invoice(request, invoice_id):
    # DANGEROUS: This fetches *any* invoice by its ID,
    # regardless of who owns it. An attacker can change `invoice_id`.
    invoice = Invoice.objects.get(pk=invoice_id)
    return render(request, 'invoice_detail.html', {'invoice': invoice})

Vulnerable Scenario 2: An API Update Endpoint

A DRF view that allows updating a user profile, but only checks for authentication.
# api/views.py
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated

class ProfileUpdateView(APIView):
    permission_classes = [IsAuthenticated] # Checks login, not ownership
    
    def post(request, user_id_from_url):
        # DANGEROUS: An attacker can be logged in as user 123,
        # but send a request to this endpoint with user_id_from_url=456
        # and update another user's profile.
        user_to_update = User.objects.get(pk=user_id_from_url)
        user_to_update.email = request.data.get('email')
        user_to_update.save()
        return Response(status=200)

Mitigation and Best Practices

Modify the query to also filter on user=request.user. For the API, the endpoint should not take a user_id from the URL; it should always operate on request.user.

Secure Code Example

# invoicing/views.py (Secure)
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404
from .models import Invoice

@login_required
def get_invoice(request, invoice_id):
    # SECURE: The query now checks that the invoice's 'user'
    # field matches the currently authenticated user.
    invoice = get_object_or_404(Invoice, pk=invoice_id, user=request.user)
    return render(request, 'invoice_detail.html', {'invoice': invoice})
    
# api/views.py (Secure)
class ProfileUpdateView(APIView):
    permission_classes = [IsAuthenticated]
    
    def post(request):
        # SECURE: The object being modified is the logged-in
        # user, not one specified in the URL.
        user_to_update = request.user 
        user_to_update.email = request.data.get('email')
        user_to_update.save()
        return Response(status=200)

Testing Strategy

Write an integration test. Create two users, user_a and user_b. Create an invoice that belongs to user_b. Log in as user_a. Attempt to GET the URL for user_b’s invoice. Assert the response is a 404 Not Found.
# invoicing/tests.py
def test_user_cannot_access_another_users_invoice(self):
    user_a = User.objects.create_user('user_a', ...)
    user_b = User.objects.create_user('user_b', ...)
    invoice_b = Invoice.objects.create(user=user_b, ...)
    
    self.client.login(username='user_a', password='...')
    response = self.client.get(reverse('get-invoice', args=[invoice_b.id]))
    
    self.assertEqual(response.status_code, 404)