Time Of Check Time Of Use in Django with Basic Auth
Time Of Check Time Of Use in Django with Basic Auth — how this specific combination creates or exposes the vulnerability
Time Of Check Time Of Use (TOCTOU) is a class of race condition where a system checks a condition (such as permissions) and later uses the result of that check to make a decision, but the state can change between the check and the use. In Django, combining Basic Authentication with authorization checks can create TOCTOU when permissions are validated separately from the actual resource access, especially when credentials are re-validated per request or when group/permission membership is mutable at runtime.
With Basic Auth, Django typically authenticates the user on each request using HTTPBasicAuthentication (from Django REST Framework) or via custom middleware that sets request.user. If an authorization check (for example, verifying group membership or a boolean flag on the user) occurs before a sensitive operation, and the user’s group or permissions are changed by another process between the check and the operation, the original check becomes stale. An attacker who can alter group membership or permissions (for example, through an administrative endpoint or a compromised account) can change state after the check but before the use, leading to unauthorized access or data exposure.
A concrete example: a view checks whether request.user.is_in_group('billing'), then proceeds to process or return a sensitive invoice. If an attacker triggers a concurrent update to the user’s group membership after the check passes but before the invoice data is accessed, the check no longer reflects the current state, and the sensitive data may be disclosed or modified. This is a TOCTOU because the authorization decision is split across time and relies on mutable backend state.
Django’s default user model stores permissions and groups in related database tables. If queries for permissions/groups are not performed atomically with the operation (for example, using select_for_update or ensuring checks happen immediately before use within the same transaction), race conditions can be exploited. Basic Auth exacerbates this when tokens or credentials are reused across requests, and the backend state is not locked or validated at the moment of use.
To detect this pattern, scans look for sequences where an authorization check (group/permission or attribute check) and the sensitive action are not performed within a single, atomic operation or transaction. Findings will highlight missing row-level locking, non-atomic check-then-act patterns, and use of mutable user attributes without re-validation at the point of use.
Basic Auth-Specific Remediation in Django — concrete code fixes
Remediation focuses on ensuring the authorization check and the use of the resource occur atomically and with up-to-date state. Avoid split checks and ensure permissions/groups are re-validated immediately before the operation, or use database-level constraints and locking to prevent state changes between check and use.
1) Use select_for_update within a transaction
When operating on sensitive resources, lock the relevant rows for the duration of the transaction so that group or permission changes cannot occur between check and use.
from django.db import transaction
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from myapp.models import Invoice
@login_required
def get_invoice(request, invoice_id):
with transaction.atomic():
# Lock the invoice row and re-check permission immediately before use
invoice = Invoice.objects.select_for_update().get(pk=invoice_id)
if not request.user.has_perm('billing.can_view_invoice', invoice):
return JsonResponse({'error': 'Forbidden'}, status=403)
# Safe to use invoice here
return JsonResponse({'id': invoice.id, 'amount': invoice.amount})
2) Re-validate permissions immediately before use
Perform authorization checks as close as possible to the operation, avoiding earlier checks that can become stale.
from django.http import JsonResponse
from myapp.models import Document
def download_document(request, doc_id):
# Authenticated by Basic Auth middleware earlier; re-validate at point of use
document = Document.objects.get(pk=doc_id)
if not request.user.has_object_permission(document, 'view'):
return JsonResponse({'error': 'Forbidden'}, status=403)
# Proceed with safe access
return JsonResponse({'url': document.file_url})
3) Use Django’s built-in permission checks and avoid mutable group-based checks in critical paths
Prefer model-level permissions and object permissions that are evaluated at the query or instance level rather than group membership that can change concurrently.
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.views.generic import DetailView
from myapp.models import Report
class ReportDetailView(PermissionRequiredMixin, DetailView):
model = Report
permission_required = ('app.view_report',)
# Ensure the permission check runs immediately before rendering
4) Employ row ownership and immutable audit fields where feasible
Design schemas so that sensitive operations rely on immutable ownership fields or tokens that cannot be changed by other processes mid-flow, reducing reliance on mutable group/permission checks.
from django.db import models
class SecureRecord(models.Model):
owner = models.ForeignKey('auth.User', on_delete=models.CASCADE)
token = models.CharField(max_length=64) # immutable per-record token
# other fields…
class Meta:
indexes = []
5) API-level mitigation with DRF’s permission classes
If using Django REST Framework with Basic Auth, use permission classes that re-check object permissions on each request and avoid broad group checks.
from rest_framework import permissions
class InvoiceObjectPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# Re-validates on each request, preventing stale group checks
return request.user.has_perm('billing.can_view_invoice', obj)
# Usage in a viewset
from rest_framework import viewsets
from myapp.models import Invoice
from myapp.serializers import InvoiceSerializer
class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Invoice.objects.all()
serializer_class = InvoiceSerializer
permission_classes = [InvoiceObjectPermission]