HIGH privilege escalationdjangobearer tokens

Privilege Escalation in Django with Bearer Tokens

Privilege Escalation in Django with Bearer Tokens — how this specific combination creates or exposes the vulnerability

Django’s default authentication stack is built around session cookies and user-centric models. When Bearer Tokens are introduced—commonly for APIs, mobile clients, or Single-Page Applications—the framework’s assumptions about authentication boundaries can break if token handling is inconsistent. Privilege Escalation in this context occurs when a token issued with limited scopes or a low-privilege identity is accepted as proof of a higher-privilege identity, or when endpoints that should enforce strict authorization fail to validate token ownership and permissions.

One common root cause is incomplete scope or role validation. A token might contain a scope like read:reports, but the view relies on Django’s user.is_staff or a group check without cross-referencing the token’s actual claims. If the token is issued by an external identity provider or an internal token service, and Django does not validate issuer, audience, or expiration rigorously, an attacker can reuse an old or low-privilege token and assume elevated permissions if the backend only checks presence rather than correctness.

Another vector is endpoint misalignment with token-bound authorization. Consider an API that uses path-based identifiers (e.g., /api/reports/{report_id}) and relies on the authenticated user’s ID from the token. If the view loads a report by ID and then checks whether the current user’s ID matches the report’s owner, but fails to ensure the token’s subject matches the Django user model, an attacker can manipulate the URL to access another user’s report while presenting a valid but low-privilege token. This is a Broken Level of Authorization (BOLA) pattern that can be triggered through Bearer Tokens when ownership checks are incomplete.

Middleware and permission class interactions can also contribute. Django REST Framework (DRF) introduces permission classes and authentication classes; if Bearer token authentication is implemented as a custom authentication class that sets request.user but does not consistently propagate role or scope metadata into permissions, views may default to permissive behavior. For example, using IsAuthenticated without a complementary scope check can allow a token intended for read-only operations to perform write actions if the view does not explicitly inspect scopes.

Token leakage and storage further amplify escalation risks. If Bearer Tokens are logged, cached improperly, or transmitted over non-TLS channels, an attacker can capture a low-privilege token and attempt to elevate by exploiting missing binding between token usage context and backend validation. Without runtime checks tying token usage to the expected client, IP, or nonce patterns, the API surface remains vulnerable to token replay and substitution attacks that facilitate privilege escalation.

Bearer Tokens-Specific Remediation in Django — concrete code fixes

Remediation centers on strict validation of token claims, consistent identity mapping, and explicit scope/role enforcement at the view or permission layer. Below are concrete, realistic code examples for Django and Django REST Framework that demonstrate secure handling of Bearer Tokens.

1. Validate issuer, audience, and scopes in authentication

Use a library such as PyJWT to decode and validate token claims before establishing request.user. Never trust the payload alone; enforce expected issuer and audience values.

import jwt
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.http import HttpResponseForbidden

def jwt_bearer_token_authentication(get_response):
    def middleware(request):
        auth_header = request.headers.get('Authorization', '')
        if auth_header.startswith('Bearer '):
            token = auth_header.split(' ')[1]
            try:
                # Validate signature, issuer, audience, and expiry
                decoded = jwt.decode(
                    token,
                    key=settings.JWT_PUBLIC_KEY,
                    algorithms=['RS256'],
                    issuer='https://auth.example.com/',
                    audience='middlebrick-api',
                    options={'require': ['exp', 'iss', 'aud', 'scope', 'sub']}
                )
                # Map token subject to a Django user (simplified)
                user = User.objects.filter(external_subject=decoded['sub']).first()
                if user is None:
                    user = AnonymousUser()
                # Attach scopes for later checks
                request.scopes = decoded.get('scope', '').split()
                request.user = user
            except jwt.PyJWTError:
                return HttpResponseForbidden('Invalid token')
        return get_response(request)
    return middleware

2. Enforce scope-based permissions in DRF

Define a custom permission that checks both user-level rights and token scopes. This prevents escalation where a token lacks required scope even if the user is authenticated.

from rest_framework.permissions import BasePermission

class ScopeRequired(BasePermission):
    def __init__(self, required_scope):
        self.required_scope = required_scope

    def has_permission(self, request, view):
        # Ensure scopes are populated by authentication middleware
        if not hasattr(request, 'scopes'):
            return False
        return self.required_scope in request.scopes

class ReportView(APIView):
    permission_classes = [IsAuthenticated, ScopeRequired]

    def get(self, request, report_id):
        # Ownership check using request.user, not token subject alone
        report = get_object_or_404(Report, pk=report_id)
        if report.owner_id != request.user.id:
            raise PermissionDenied('Not owner')
        return Response({'data': report.data})

3. Bind token usage to request context

When handling sensitive operations, bind token usage to additional request context such as IP or a nonce stored server-side. This reduces the impact of token leakage.

from django.core.cache import cache

def validate_token_binding(request, token_sub):
    cache_key = f'token_nonce:{token_sub}'
    expected_nonce = cache.get(cache_key)
    if expected_nonce != request.META.get('HTTP_X_TOKEN_NONCE'):
        return False
    return True

4. Centralize authorization checks at the view or service layer

Do not rely on object-level permissions alone; ensure every data access path validates that the token’s subject maps correctly to the resource owner and that the token includes necessary scope for the action.

def get_ordered_report(request, report_id):
    report = get_object_or_404(Report, pk=report_id)
    if not request.user.has_perm('view_report', report):
        raise PermissionDenied
    # scope check for sensitive operations
    if 'write:reports' not in getattr(request, 'scopes', []):
        raise PermissionDenied
    return report

Frequently Asked Questions

How does middleBrick detect privilege escalation risks related to Bearer Tokens in Django?
middleBrick runs unauthenticated checks that validate whether token-based authentication consistently enforces scope and ownership. It cross-references OpenAPI definitions with runtime behavior to identify endpoints where authentication does not enforce proper authorization or where identity mapping is inconsistent, flagging BOLA/IDOR and privilege escalation findings with remediation guidance.
Can Bearer Tokens in Django be secured without changing the frontend client?
Yes, security can be strengthened on the backend by enforcing strict token validation (issuer, audience, scopes), binding tokens to request context, and using scope-based permission classes. These measures reduce escalation risk without requiring frontend changes, though clients must send tokens in the Authorization header as Bearer tokens.