Bleichenbacher Attack in Django with Basic Auth
Bleichenbacher Attack in Django with Basic Auth — how this specific combination creates or exposes the vulnerability
A Bleichenbacher attack is a cryptographic side-channel attack originally described against RSA PKCS#1 v1.5 padding. In the context of Django with HTTP Basic Authentication, the term refers to an attacker using timing differences and error messages to infer whether a given credential is valid, without needing to know the password. When Django’s built-in Basic Authentication runs, it retrieves the user for the provided username and then checks the password. If the username does not exist, Django can raise a distinct path (user not found) compared to a valid username with an incorrect password (password mismatch). These differences can manifest as small timing variations or in the content and structure of HTTP 401 responses, giving an attacker a side channel to iteratively guess a valid username and eventually recover the password through many carefully crafted requests.
In a black-box scan, middleBrick runs authentication and authorization checks as part of its 12 parallel security checks. For an endpoint protected with Basic Auth, it tests whether the server’s responses differ measurably between a nonexistent user and a valid user with a wrong password. If such differences are detectable, the scan flags findings under Authentication and under BOLA/IDOR-related checks, because the server’s behavior leaks information that should remain constant regardless of credential validity. This is especially relevant when the API does not enforce uniform error handling and rate limiting, allowing an attacker to mount low-rate, long-duration guesses without triggering defenses. Even without direct access to server logs, an attacker can observe HTTP status codes and response times to refine their guesses, emulating techniques similar to adaptive chosen-ciphertext attacks adapted to the application layer.
Django’s default behavior with Basic Auth does not inherently encrypt or hide the fact that a username does not exist; it simply returns a 401 with a WWW-Authenticate header. If the view does not wrap authentication in a constant-time comparison and does not normalize error responses, subtle timing and response differences become exploitable. Compounded with missing or weak rate limiting, an attacker can automate requests, measure round-trip times, and observe slight delays for valid usernames. The presence of Basic Auth over unencrypted HTTP further exacerbates the issue, as credentials travel in base64-encoded form and are trivial to intercept, while the side channel operates on the server’s response pattern. Proper remediation therefore focuses on eliminating observable differences in authentication flows and ensuring that all credentials are protected in transit.
Basic Auth-Specific Remediation in Django — concrete code fixes
To mitigate timing and information-leak concerns with Basic Auth in Django, ensure that authentication checks follow a uniform path regardless of whether the username exists. This means performing a constant-time password check and returning the same generic 401 response for any failed authentication. Below are concrete, working examples that you can adapt to your views or authentication backends.
Example 1: Constant-time check in a view using Django’s built-in authenticate
import secrets
import hashlib
import hmac
from django.http import HttpResponse
from django.contrib.auth import authenticate, get_user_model
User = get_user_model()
# A fixed, dummy hash to use when the user is not found, to force constant-time compare
def _dummy_hash():
return secrets.token_bytes(64)
def verify_password_in_constant_time(stored_hash: bytes, provided_password: str) -> bool:
"""Compare a stored hash with a provided password in constant time."""
if stored_hash is None:
stored_hash = _dummy_hash()
# Use hmac.compare_digest to avoid timing leaks
return hmac.compare_digest(stored_hash, hashlib.sha256(provided_password.encode("utf-8")).digest())
def my_protected_view(request):
auth_header = request.META.get("HTTP_AUTHORIZATION", "")
if not auth_header.startswith("Basic "):
return HttpResponse("Unauthorized", status=401, headers={"WWW-Authenticate": 'Basic realm="api"'})
import base64
try:
decoded = base64.b64decode(auth_header.split(" ", 1)[1])
username, _, password = decoded.decode("utf-8").partition(":")
except Exception:
return HttpResponse("Unauthorized", status=401, headers={"WWW-Authenticate": 'Basic realm="api"'})
# Always fetch the user to avoid username enumeration via timing
try:
user = User.objects.get(username=username)
stored_hash = user.password.encode("utf-8") if user.password else None
except User.DoesNotExist:
stored_hash = None
# Perform constant-time verification
if verify_password_in_constant_time(stored_hash, password):
# Log the user in or proceed with request processing
return HttpResponse("OK")
else:
return HttpResponse("Unauthorized", status=401, headers={"WWW-Authenticate": 'Basic realm="api"'})
Example 2: Custom authentication backend with uniform failure behavior
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth import get_user_model
import secrets
import hashlib
import hmac
User = get_user_model()
def _dummy_hash():
return secrets.token_bytes(64)
class ConstantTimeBasicBackend(BaseBackend):
def authenticate(self, request, username=None, password=None):
try:
user = User.objects.get(username=username)
stored = user.password.encode("utf-8") if user.password else None
except User.DoesNotExist:
stored = None
if stored is None:
stored = _dummy_hash()
if hmac.compare_digest(stored, hashlib.sha256(password.encode("utf-8")).digest()):
return user
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
Additionally, pair these code-level changes with operational safeguards: enforce HTTPS to protect credentials in transit, apply global rate limiting (for example via Django middleware or an external gateway) to slow down iterative guessing, and ensure error responses do not distinguish between “user not found” and “invalid password.” The middleBrick CLI can be used to scan your endpoints and validate that authentication and error handling exhibit uniform timing and response characteristics, while the GitHub Action can enforce security gates in CI/CD pipelines to prevent regressions.