Timing Attack in Django with Basic Auth
Timing Attack in Django with Basic Auth — how this specific combination creates or exposes the vulnerability
A timing attack in Django when using HTTP Basic Auth becomes feasible because authentication logic can short-circuit on the first incorrect character of the secret, allowing an attacker to infer valid credentials through measured response times. In Django, if you implement a custom login view that manually compares a provided password against a stored secret (for example, a raw token or API key stored as a string) using a naive character-by-character comparison, the comparison may exit early on mismatch. This behavior introduces measurable differences in response time that correlate with how many leading characters match.
Consider an endpoint that expects a static API key passed via the Authorization header as Basic Auth (username can be a fixed value, and the password is the secret). A vulnerable implementation might decode the header and compare the provided secret with the expected value in Python using equality (==) in a way that does not run in constant time. Because Basic Auth credentials are base64-encoded rather than cryptographically protected in transit (always use HTTPS), an attacker who can measure response times—potentially via network latencies or side channels—can iteratively guess characters. Over many requests, the attacker builds a profile of which prefixes cause slightly longer server processing, effectively leaking the secret one character at a time.
Django’s built-in authenticate() for user/password pairs relies on secure password hashing (e.g., PBKDF2), which is designed to be slow and constant-time in its final comparison, reducing this risk for traditional user authentication. However, when developers use Basic Auth for API tokens and implement their own verification—decoding the header and comparing raw strings—they bypass those protections. The framework itself does not introduce the flaw; the risk arises from how the developer compares the credentials. Therefore, the combination of Django as the web framework and Basic Auth used for token-based authentication creates a vulnerability surface if string comparisons are not constant-time.
Moreover, network-level variability can amplify the signal. Even if framework-level comparison is constant-time, underlying socket reads and WSGI server behavior might still introduce minor timing variations. When an attacker can make many authenticated requests (for example, by automating curl commands or lightweight scripts) and observe response times or side effects (such as different HTTP status codes or response body content), they can distinguish correct prefixes from incorrect ones. This is especially relevant when the endpoint returns different processing paths depending on authentication success, effectively turning timing discrepancies into an oracle.
To detect this class of issue, scanners like middleBrick perform unauthenticated black-box checks that look for indicators such as custom Basic Auth handling, absence of rate limiting, and endpoints where response characteristics vary with credential inputs. They do not attempt to exploit or prove exploitability but highlight where behavior deviates from constant-time expectations and recommend remediation aligned with secure coding practices.
Basic Auth-Specific Remediation in Django — concrete code fixes
Remediation focuses on ensuring any comparison of secrets runs in constant time and avoiding custom verification for credentials handled via Basic Auth. Use Django’s built-in password hashing for user credentials; for API tokens or static secrets, store a hash and compare hashes using a constant-time function. Never compare raw secrets directly.
Example of a vulnerable view that decodes Basic Auth and compares raw strings:
import base64
from django.http import HttpResponse, HttpResponseForbidden
from django.views import View
class VulnerableBasicAuthView(View):
# WARNING: This comparison is not constant-time and leaks timing information
def post(self, request):
auth = request.META.get('HTTP_AUTHORIZATION', '')
if auth.startswith('Basic '):
encoded = auth.split(' ')[1]
decoded = base64.b64decode(encoded).decode('utf-8')
# Assume format "username:password"; password is the secret
_, password = decoded.split(':', 1)
if password == 'my-super-secret-token': # Unsafe
return HttpResponse('OK')
return HttpResponseForbidden()
The comparison password == 'my-super-secret-token' can short-circuit and is vulnerable to timing attacks. An attacker observing response times can infer the secret character by character.
Secure alternative using constant-time comparison:
import base64
import hmac
from django.http import HttpResponse, HttpResponseForbidden
from django.views import View
class SecureBasicAuthView(View):
EXPECTED_TOKEN = 'my-super-secret-token' # In practice, store a hash
def post(self, request):
auth = request.META.get('HTTP_AUTHORIZATION', '')
if auth.startswith('Basic '):
encoded = auth.split(' ')[1]
decoded = base64.b64decode(encoded).decode('utf-8')
_, password = decoded.split(':', 1)
# Use hmac.compare_digest for constant-time comparison
if hmac.compare_digest(password, self.EXPECTED_TOKEN):
return HttpResponse('OK')
return HttpResponseForbidden()
hmac.compare_digest ensures the comparison time does not depend on the number of matching characters, mitigating timing-based inference. For production, store a salted, hashed version of the token and compare the hash using a constant-time function, or use Django’s check_password if treating the token as a password-equivalent secret.
Additionally, enforce HTTPS to protect credentials in transit, apply rate limiting to prevent brute-force attempts, and avoid exposing whether a username exists. Combine these measures with middleware or decorators to centralize secure authentication logic across any view that uses Basic Auth.