Credential Stuffing in Django
How Credential Stuffing Manifests in Django
Credential stuffing attacks target the login endpoint of a Django application by automating requests with large lists of username/password pairs harvested from previous breaches. Because Django’s default authentication backend (django.contrib.auth.backends.ModelBackend) checks credentials against the database using check_password, an attacker can iterate through millions of combos if the view lacks any throttling or anomaly detection.
Typical vulnerable code paths include:
- A custom login view that directly calls
authenticate(request, username=username, password=password)without checking request frequency. - The built‑in
django.contrib.auth.views.LoginViewwhen used without additional throttling middleware. - API endpoints built with Django REST Framework that expose
obtain_auth_tokenorTokenObtainPairView(JWT) and rely solely on serializer validation.
When successful, the attacker gains an authenticated session (session cookie or token) and can perform actions as the compromised user. This maps to OWASP API2:2019 Broken Authentication and has been observed in real‑world incidents such as the 2020 credential stuffing campaign against multiple SaaS platforms that used Django‑based backends.
Django-Specific Detection
Detecting credential stuffing in Django requires observing patterns that differ from normal user behavior: a high volume of failed logins from a single IP address or targeting many usernames with the same password, and vice‑versa. Django provides several hooks for visibility:
- The
user_login_failedsignal fires on every failed authentication attempt. - Custom middleware can inspect
request.META['REMOTE_ADDR']and the submitted username to count failures. - Django’s logging (
LOGGINGconfiguration) can capture authentication warnings fromdjango.contrib.auth.
Example detection middleware that logs and thresholds failed attempts:
# middleware/credential_stuffing.py
from django.core.cache import cache
from django.http import HttpResponseTooManyRequests
import hashlib
class CredentialStuffingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.limit = 10 # attempts
self.window = 60 # seconds
def __call__(self, request):
if request.path == '/accounts/login/' and request.method == 'POST':
ip = request.META.get('REMOTE_ADDR', '0.0.0.0')
username = request.POST.get('username', '')
key = f'login_fail:{ip}:{hashlib.sha256(username.encode()).hexdigest()}'
fails = cache.get(key, 0)
if fails >= self.limit:
return HttpResponseTooManyRequests('Too many login attempts')
response = self.get_response(request)
return response
def process_response(self, request, response):
if request.path == '/accounts/login/' and request.method == 'POST':
if response.status_code == 401 or response.status_code == 403:
ip = request.META.get('REMOTE_ADDR', '0.0.0.0')
username = request.POST.get('username', '')
key = f'login_fail:{ip}:{hashlib.sha256(username.encode()).hexdigest()}'
cache.incr(key, delta=1)
cache.expire(key, self.window)
return response
When this middleware is added to MIDDLEWARE, it will begin throttling after ten failed attempts from the same IP/username pair within a minute.
middleBrick can detect credential‑stuffing risk without any code changes: by submitting the API URL, the scanner performs unauthenticated black‑box tests that include rapid credential spraying against login endpoints. If the endpoint responds with successful authentication (200 OK) for multiple guessed pairs, middleBrick reports a finding under the “Authentication” category with severity High, includes the observed success rate, and provides remediation guidance.
Django-Specific Remediation
Effective mitigation combines Django’s native security features with disciplined rate limiting and monitoring. The following steps are recommended:
- Use Django’s built‑in authentication views (
LoginView) and ensure they are protected by a throttling mechanism. - Implement per‑IP and/or per‑username rate limiting via middleware or a third‑party package such as
django-ratelimit. - Employ constant‑time comparison for any custom password checks to avoid timing attacks (
django.utils.crypto.constant_time_compare). - Enable the
user_login_failedsignal to alert on anomalous patterns (e.g., sudden spike in failures). - Consider account lockout after a configurable number of failed attempts, but combine with CAPTCHA to prevent denial‑of‑service.
- Regularly review Django’s security releases; patches for authentication‑related issues are issued as CVEs (e.g., CVE-2020-XXXXX for password‑reset token handling).
Example of a throttled login view using django-ratelimit:
# views.py
from django.contrib.auth.views import LoginView
from django_ratelimit.decorators import ratelimit
@ratelimit(key='ip', rate='5/m', method='POST', block=True)
class ThrottledLoginView(LoginView):
template_name = 'registration/login.html'
# AuthenticationForm is used by default; ensures constant‑time password checking
# urls.py
from django.urls import path
from .views import ThrottledLoginView
urlpatterns = [
path('accounts/login/', ThrottledLoginView.as_view(), name='login'),
]
If you prefer not to add external dependencies, the custom middleware shown in the Detection section can be extended to return a 429 response and optionally present a CAPTCHA challenge.
After applying these controls, re‑run middleBrick (e.g., middlebrick scan https://api.example.com) to verify that the Authentication score improves. The tool will continue to report any residual risk, allowing you to track progress over time via the Dashboard or through the GitHub Action that can fail a build if the score drops below your defined threshold.
Frequently Asked Questions
Does middleBrick need any credentials or agents to test my Django login endpoint for credential stuffing?
Can I integrate the detection of credential‑stuffing failures from my Django application into middleBrick’s continuous monitoring?
user_login_failed signal) into your SIEM and correlate them with middleBrick’s periodic scan results to get a unified view of authentication risk.