Time Of Check Time Of Use in Django
How Time Of Check Time Of Use Manifests in Django
Time Of Check Time Of Use (TOCTOU) race conditions in Django occur when an application validates a condition, then acts on it, but the underlying state changes between those operations. Django's web framework and ORM patterns create several specific TOCTOU vulnerabilities that developers should understand.
One common Django TOCTOU pattern appears in object-level permissions. Consider a view that checks if a user owns an object before allowing deletion:
def delete_post(request, post_id):
post = Post.objects.get(id=post_id)
if post.author != request.user:
return HttpResponseForbidden()
post.delete()
return JsonResponse({'status': 'deleted'})
This code has a classic TOCTOU race condition. Between the permission check and the delete operation, another request could transfer ownership of the post to a different user. The check passes, but the delete occurs on an object the requesting user no longer owns.
Another Django-specific TOCTOU scenario involves model state changes during multi-step operations. When updating related objects, the initial state validation can become stale:
def update_order_status(request, order_id):
order = Order.objects.get(id=order_id)
if order.status != 'pending':
return JsonResponse({'error': 'Order not pending'})
# Simulate some processing time
time.sleep(2)
order.status = 'shipped'
order.save()
return JsonResponse({'status': 'updated'})
The 2-second sleep creates a window where another process could change the order status, making the initial check invalid by the time the update executes.
File-based TOCTOU attacks also affect Django applications. When serving user-uploaded content, the file existence check and subsequent access create a vulnerability window:
def serve_user_file(request, filename):
filepath = os.path.join(settings.MEDIA_ROOT, filename)
if not os.path.exists(filepath):
return HttpResponseNotFound()
# TOCTOU window: file could be replaced between check and open
with open(filepath, 'rb') as f:
return FileResponse(f)
Between the os.path.exists() check and the open() call, an attacker could replace the file with a symlink pointing to sensitive system files, enabling path traversal attacks.
Django-Specific Detection
Detecting TOCTOU vulnerabilities in Django requires both static analysis and runtime scanning. middleBrick's API security scanner includes specific checks for Django applications, identifying TOCTOU patterns through black-box testing and OpenAPI spec analysis.
For Django applications using class-based views, middleBrick analyzes the view structure to identify potential race conditions. The scanner examines view methods that perform permission checks followed by state-changing operations:
class PostDeleteView(DeleteView):
model = Post
def get_queryset(self):
# Permission check
return Post.objects.filter(author=self.request.user)
def delete(self, request, *args, **kwargs):
# TOCTOU window exists between queryset filtering and actual deletion
return super().delete(request, *args, **kwargs)
middleBrick's scanning engine tests these patterns by creating concurrent requests that attempt to manipulate object state during the vulnerable window. The scanner uses Django's test client to simulate race conditions that would be difficult to catch through manual testing.
The scanner also analyzes Django's ORM query patterns. When it detects select_for_update() missing from critical sections, it flags potential TOCTOU vulnerabilities:
# Vulnerable pattern detected by middleBrick
post = Post.objects.get(id=post_id)
if post.author == request.user:
post.delete() # No locking, race condition possible
# Recommended pattern
with transaction.atomic():
post = Post.objects.select_for_update().get(id=post_id)
if post.author == request.user:
post.delete()
middleBrick's OpenAPI analysis extends to Django REST Framework applications. When scanning DRF serializers and views, it identifies TOCTOU patterns in nested update operations:
class OrderUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ['status', 'items']
def update(self, instance, validated_data):
# TOCTOU vulnerability: items could change between validation and update
if instance.status != 'pending':
raise serializers.ValidationError('Order not pending')
instance.status = validated_data.get('status', instance.status)
instance.save()
return instance
The scanner flags this because the status check and subsequent update aren't atomic, creating a window for concurrent modifications.
Django-Specific Remediation
Remediating TOCTOU vulnerabilities in Django requires understanding when atomic operations are necessary and using Django's built-in features to eliminate race conditions. The most effective approach is using database transactions with row-level locking.
For object deletion with permission checks, use select_for_update() within a transaction:
from django.db import transaction
from django.http import JsonResponse
@transaction.atomic
def delete_post(request, post_id):
post = Post.objects.select_for_update().get(id=post_id)
if post.author != request.user:
return JsonResponse({'error': 'Forbidden'}, status=403)
post.delete()
return JsonResponse({'status': 'deleted'})
The select_for_update() locks the row until the transaction commits, preventing other transactions from modifying it during the critical section. This eliminates the TOCTOU window entirely.
For status-based operations, always validate state within the same transaction:
from django.db import transaction
@transaction.atomic
def update_order_status(request, order_id):
order = Order.objects.select_for_update().get(id=order_id)
if order.status != 'pending':
raise serializers.ValidationError('Order must be pending')
order.status = 'shipped'
order.save()
return order
Django's F() expressions provide another remediation pattern for concurrent updates:
from django.db.models import F
# Instead of:
post = Post.objects.get(id=post_id)
if post.view_count >= 0:
post.view_count += 1
post.save()
# Use atomic update:
Post.objects.filter(id=post_id).update(view_count=F('view_count') + 1)
This approach eliminates the read-modify-write cycle entirely, making it immune to TOCTOU attacks.
For file-based operations, use atomic file operations and validate file integrity:
import os
import hashlib
from django.conf import settings
from django.http import FileResponse, HttpResponseNotFound
def serve_user_file(request, filename):
filepath = os.path.join(settings.MEDIA_ROOT, filename)
# Atomic existence check with file descriptor
try:
with open(filepath, 'rb', opener=os.open, flags=os.O_RDONLY|os.O_NOFOLLOW) as f:
# Verify file is still the expected file
file_hash = hashlib.sha256(f.read()).hexdigest()
if file_hash != expected_hash:
return HttpResponseNotFound()
f.seek(0)
return FileResponse(f)
except (FileNotFoundError, OSError):
return HttpResponseNotFound()
This pattern uses os.O_NOFOLLOW to prevent symlink attacks and verifies file integrity before serving, eliminating the TOCTOU window for path traversal attacks.