Skip to main content

Overview

Improper Access Control occurs when an application fails to properly enforce what a user is allowed to do. This is different from authentication (who you are). This vulnerability means an authenticated user (e..g., a “viewer”) can perform actions reserved for a different role (e.g., an “admin”), such as accessing an admin panel or deleting data.

Business Impact

This is a critical vulnerability that can lead to full system compromise. A low-privilege attacker can escalate their privileges, modify or delete any data, and lock out legitimate administrators.

Reference Details

CWE ID: CWE-284 OWASP Top 10 (2021): A01:2021 - Broken Access Control Severity: High

Framework-Specific Analysis and Remediation

All frameworks provide powerful, declarative, and imperative ways to handle authorization. The vulnerability is almost always a developer forgetting to apply these controls to a new endpoint or function. The principle is “Deny by Default.”
  • Python
  • Java
  • .NET(C#)
  • PHP
  • Node.js
  • Ruby

Framework Context

Django and DRF provide view decorators (@login_required, @permission_required) and permission_classes on views. The vulnerability is a view that lacks these.

Vulnerable Scenario 1: A Missing Permission Check

An admin view is created that only checks if the user is logged in, not if they are an admin.
# reporting/views.py
from django.contrib.auth.decorators import login_required

@login_required
def generate_all_sales_report(request):
    # DANGEROUS: Any logged-in user can access this, not just staff.
    # It should check for `is_staff` or a specific permission.
    report = Sales.objects.generate_full_report()
    return HttpResponse(report, content_type="text/csv")

Vulnerable Scenario 2: DRF ViewSet

A DRF ModelViewSet that has no permission_classes set.
# api/views.py
class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    # DANGEROUS: This ViewSet allows ANY authenticated user
    # (or even anonymous if default is AllowAny) to LIST, CREATE,
    # UPDATE, and DELETE users.
    # permission_classes = [permissions.IsAuthenticated] # Still too permissive!

Mitigation and Best Practices

For function-based views, use @user_passes_test(lambda u: u.is_staff). For class-based views, use UserPassesTestMixin. For DRF, set permission_classes explicitly, e.g., permission_classes = [permissions.IsAdminUser].

Secure Code Example

# reporting/views.py (Secure Version)
from django.contrib.auth.decorators import user_passes_test

@login_required
@user_passes_test(lambda u: u.is_staff)
def generate_all_sales_report(request):
    # SECURE: Now only staff members can access this view.
    report = Sales.objects.generate_full_report()
    return HttpResponse(report, content_type="text/csv")
    
# api/views.py (Secure Version)
from rest_framework import permissions

class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    # SECURE: Only Admin users can access this ViewSet.
    permission_classes = [permissions.IsAdminUser]

Testing Strategy

Write integration tests where you authenticate as a non-admin user. Attempt to access the admin endpoint and assert that the response is a 403 Forbidden.
# reporting/tests.py
def test_sales_report_is_forbidden_for_normal_user(self):
    # self.client is logged in as a non-staff user
    response = self.client.get(reverse('sales-report'))
    self.assertEqual(response.status_code, 403)