HIGH bleichenbacher attackdjangobasic auth

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.

Frequently Asked Questions

Why does using Basic Auth with Django expose a Bleichenbacher-style side channel?
Because Django’s default authentication path can differ when a username does not exist versus when a username exists but the password is wrong, and if those differences are observable via timing or response content, an attacker can iteratively guess credentials.
What is the most important mitigation for Basic Auth in Django against information leakage?
Use a constant-time password comparison for all authentication attempts and return identical HTTP 401 responses with the same WWW-Authenticate header regardless of whether the username exists; also enforce HTTPS and rate limiting.