Rate Limiting Bypass in Django with Basic Auth
Rate Limiting Bypass in Django with Basic Auth — how this specific combination creates or exposes the vulnerability
Rate limiting is a critical control that reduces the impact of credential stuffing, brute-force, and automated abuse. In Django, combining HTTP Basic Authentication with rate limiting can introduce a bypass when the two controls are not tightly coupled at the decision point. The vulnerability arises when rate limiting is applied before authentication, or when different identifiers are used for authentication and rate-limiting keys.
Consider a Django view that protects a sensitive endpoint with Basic Auth but applies rate limiting based only on the client IP address. An attacker who knows or can guess a valid username can iterate over passwords without being blocked if the rate limit is IP-based and the attacker’s requests all originate from a single address. Conversely, if rate limiting is applied after authentication but uses the authenticated username as the limiting key, an attacker who does not know a valid username may not consume any per-user quota, effectively bypassing the limit because no user-specific counter is incremented.
In practice, this can manifest in endpoints that expose account enumeration via timing differences or response codes. For example, a login endpoint that performs Basic Auth validation and then applies a per-IP throttle may still return different HTTP status codes or timing for valid versus invalid users before the throttle is evaluated. This enables an attacker to harvest valid usernames and then target them individually, bypassing the intended aggregate protection.
Another common pitfall is inconsistent scope in middleware. If custom middleware applies rate limiting to all requests but authentication is deferred to a decorator or view logic, the middleware may see unauthenticated requests and apply limits to IPs, while authenticated requests skip the middleware or use a different keying strategy. This inconsistency creates a window where authenticated bursts are not counted against the same bucket as unauthenticated ones.
To detect this pattern, scans such as those run by middleBrick evaluate whether rate limiting is applied uniformly across authenticated and unauthenticated paths, whether the limiting key aligns with the authentication identity, and whether timing or status-code differences leak information about valid credentials. The scanner also checks whether controls like burst and sustained limits are present for both anonymous and authenticated contexts, ensuring that an attacker cannot switch between IPs or authentication states to evade thresholds.
Remediation focuses on coupling identity and rate limiting, using stable, normalized keys that are present regardless of authentication state, and ensuring consistent middleware ordering. Security checks that map to relevant frameworks include OWASP API Top 10 2023 API5:2023 (Broken Function Level Authorization), which can be triggered when access controls and throttling are misaligned, and related patterns seen in tools like those referenced in PCI-DSS and SOC2 control frameworks.
Basic Auth-Specific Remediation in Django — concrete code fixes
Securely coupling Basic Authentication with rate limiting in Django requires a shared, deterministic key and consistent middleware placement. Use the authenticated identity when available, and fall back to a stable anonymous key such as the IP address. Below are concrete patterns that reduce the risk of bypass.
1. Middleware-based rate limiting with a normalized key
Implement a middleware that computes a rate-limit key from the request. If HTTP Basic Auth credentials are present and valid, use the username; otherwise, use the client IP. Ensure the middleware runs before the view so that throttling is applied uniformly.
import hashlib
from django.http import JsonResponse
from django.utils.deprecation import MiddlewareMixin
class BasicAuthRateLimitMiddleware(MiddlewareMixin):
def process_request(self, request):
# Determine key: username if auth present and valid, else IP
key = None
auth = request.META.get('HTTP_AUTHORIZATION', '')
if auth.startswith('Basic '):
# Decode credentials without validating them here
import base64
encoded = auth.split(' ', 1)[1].strip()
try:
decoded = base64.b64decode(encoded).decode('utf-8')
username = decoded.split(':', 1)[0]
key = f'user:{username}'
except Exception:
key = f'ip:{request.META.get("REMOTE_ADDR")}'
else:
key = f'ip:{request.META.get("REMOTE_ADDR")}'
request.rate_limit_key = key
# Continue to Django’s cache/backend based on key
# Example using a simple in-memory store; replace with Redis or similar in prod
# Placeholder: check_limit(request.rate_limit_key) raises SuspiciousOperation if exceeded
2. View-level throttling with authenticated identity
In your view, validate Basic Auth and use the username explicitly for per-user limits. Combine with an IP-based fallback for unauthenticated requests.
from django.contrib.auth.models import User
from django.http import HttpResponse, HttpResponseForbidden
from django.views import View
import base64
class SensitiveEndpoint(View):
def dispatch(self, request, *args, **kwargs):
auth = request.META.get('HTTP_AUTHORIZATION', '')
username = None
if auth.startswith('Basic '):
encoded = auth.split(' ', 1)[1].strip()
try:
decoded = base64.b64decode(encoded).decode('utf-8')
username, password = decoded.split(':', 1)
user = User.objects.filter(username=username).first()
if user is not None and user.check_password(password):
request.user = user
else:
return HttpResponseForbidden('Invalid credentials')
except Exception:
return HttpResponseForbidden('Invalid authorization header')
else:
# No auth: treat as anonymous with IP-based limit
request.user = None
return super().dispatch(request, *args, **kwargs)
def get(self, request):
# Apply per-user or per-IP logic explicitly
key = f'user:{request.user.username}' if request.user else f'ip:{request.META.get("REMOTE_ADDR")}'
# Check limit using key; return 429 if exceeded
# Placeholder: if is_limited(key): return HttpResponse(status=429)
return HttpResponse('OK')
3. Consistent keying across middleware and views
Ensure that the same normalization logic is used in both middleware and any view- or cache-level throttling. Prefer a shared utility function to derive the key so that policies do not diverge.
def get_rate_limit_key(request):
auth = request.META.get('HTTP_AUTHORIZATION', '')
if auth.startswith('Basic '):
encoded = auth.split(' ', 1)[1].strip()
try:
decoded = base64.b64decode(encoded).decode('utf-8')
username = decoded.split(':', 1)[0]
return f'user:{username}'
except Exception:
pass
return f'ip:{request.META.get("REMOTE_ADDR")}'
By normalizing the key across layers and validating credentials before applying per-user limits, you reduce the risk that one control’s scope diverges from the other. This approach aligns with secure design principles observed in assessments run by tools like middleBrick, which checks for consistency between authentication and authorization/rate-limiting mechanisms.
Related CWEs: resourceConsumption
| CWE ID | Name | Severity |
|---|---|---|
| CWE-400 | Uncontrolled Resource Consumption | HIGH |
| CWE-770 | Allocation of Resources Without Limits | MEDIUM |
| CWE-799 | Improper Control of Interaction Frequency | MEDIUM |
| CWE-835 | Infinite Loop | HIGH |
| CWE-1050 | Excessive Platform Resource Consumption | MEDIUM |