Credential Stuffing in Django with Basic Auth
Credential Stuffing in Django with Basic Auth — how this specific combination creates or exposes the vulnerability
Credential stuffing is an automated attack in which lists of breached username and password pairs are systematically tried against an endpoint to find valid accounts. When Django is configured to use HTTP Basic Authentication, the risk profile changes in ways that can unintentionally aid attackers or leak information.
Basic Auth sends credentials with every request as a Base64-encoded string in the Authorization header. While the header itself is not inherently unsafe when used over HTTPS, the way Django processes authentication can expose behaviors useful to an attacker. For example, many Django applications expose a login or admin view that accepts Basic Auth. If the endpoint does not enforce strict rate limiting or does not mask whether a username exists, an attacker can probe with credential lists and infer valid accounts based on HTTP status codes (e.g., 200 OK for success versus 401 Unauthorized for failure).
Django’s built-in django.contrib.auth and HTTP Basic implementations do not inherently prevent credential reuse. If session cookies or tokens are issued after successful Basic Auth without additional protections such as multi-factor authentication or short-lived credentials, attackers can replay captured credentials. Misconfigured CORS or mixed content (HTTP vs HTTPS) can further expose credentials in transit. Additionally, if logging inadvertently records headers, plaintext credentials may appear in logs, increasing exposure risk.
Another subtlety is that Django’s authentication backends can be extended. Custom backends that accept Basic Auth may not enforce per-user lockouts or may inadvertently reveal stack traces or verbose errors, which attackers can exploit to refine stuffing campaigns. Without explicit protections such as exponential backoff or IP-based throttling, Basic Auth endpoints become attractive targets for low-and-slow credential stuffing attempts that evade simple rate limits.
Basic Auth-Specific Remediation in Django — concrete code fixes
Securing Basic Auth in Django requires deliberate controls around authentication flow, error handling, and transport. Below are concrete code examples that demonstrate safer configurations.
1. Require HTTPS and reject cleartext requests
Ensure your Django project rejects non-HTTPS requests when Basic Auth is used. This prevents credentials from traversing the network in base-readable form.
import os
from django.conf import settings
class SSLRequiredMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if not request.is_secure() and request.META.get('HTTP_AUTHORIZATION', '').startswith('Basic '):
from django.http import HttpResponseForbidden
return HttpResponseForbidden('HTTPS required.')
return self.get_response(request)
Add this middleware early in MIDDLEWARE and set SECURE_SSL_REDIRECT = True in production settings.
2. Use Django REST Framework Basic Authentication with custom error handling
DRF provides built-in Basic Auth. Override the authentication failure behavior to avoid leaking whether a username exists.
from rest_framework.authentication import BasicAuthentication
from rest_framework.exceptions import AuthenticationFailed
from django.http import JsonResponse
class SafeBasicAuthentication(BasicAuthentication):
def authenticate(self, request):
auth = super().authenticate(request)
if auth is None:
# Always return the same generic failure response
raise AuthenticationFailed('Invalid credentials.')
return auth
Use this class in your views or viewsets via the authentication_classes attribute.
3. Enforce rate limiting per username or IP
Prevent rapid credential trials by applying throttling at the API level.
from rest_framework.throttling import UserRateThrottle
class BasicAuthThrottle(UserRateThrottle):
rate = '5/minute' # tune to your risk profile
scope = 'basic_auth'
# In views or viewsets:
# from rest_framework.decorators import throttle_classes
# throttle_classes = [BasicAuthThrottle]
Consider a custom throttle that combines user and IP to reduce enumeration risk.
4. Avoid verbose authentication errors and hide stack traces
Set DEBUG = False and use a standardized error handler to ensure authentication failures do not expose stack traces or usernames.
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'myapp.exceptions.custom_exception_handler',
}
# myapp/exceptions.py
from rest_framework.views import exception_handler as drf_exception_handler
from rest_framework.response import Response
def custom_exception_handler(exc, context):
response = drf_exception_handler(exc, context)
if response is not None and response.status_code == 401:
response.data = {'detail': 'Authentication failed.'}
return response
5. Rotate credentials and avoid embedding secrets in source
Treat static Basic Auth credentials as high-risk. Rotate them regularly and avoid placing them in version control. Use environment variables and secret management integrations.
import os
from django.http import JsonResponse
from django.views import View
from django.contrib.auth import authenticate
class ProtectedView(View):
def get(self, request):
provided = request.META.get('HTTP_AUTHORIZATION', '')
if not provided.startswith('Basic '):
return JsonResponse({'error': 'Unauthorized'}, status=401)
# Validate via a rotating token stored in environment
expected = os.getenv('BASIC_AUTH_TOKEN')
if expected is None or provided != f'Basic {expected}':
return JsonResponse({'error': 'Unauthorized'}, status=401)
return JsonResponse({'status': 'ok'})