Brute Force Attack in Django with Mutual Tls
Brute Force Attack in Django with Mutual Tls — how this specific combination creates or exposes the vulnerability
A brute force attack targets authentication endpoints by systematically trying many credentials. In Django, mutual Transport Layer Security (mTLS) adds client certificate verification on top of server-side TLS. While mTLS strengthens identity assurance, it does not inherently prevent credential guessing. If an endpoint is protected primarily by client certificates but still accepts arbitrary username/password pairs, attackers can iterate through accounts or passwords once the TLS handshake (including client cert validation) succeeds. This shifts the effective attack surface to the application layer because the network-level check passes, but Django’s authentication logic remains the gatekeeper for user identity.
Django’s default authentication views and APIs do not automatically enforce rate limiting per client certificate. Without additional controls, an attacker holding a valid client certificate can perform rapid, unthrottled requests against login or password-reset endpoints. Tools that test unauthenticated attack surfaces can detect whether mTLS-enabled endpoints exhibit missing rate limiting or weak lockout policies. Even with mTLS, common patterns such as session fixation or insecure direct object references (BOLA/IDOR) may persist if Django permissions are misconfigured, allowing an authenticated client to probe other users’ resources.
The combination of mTLS and Django can also create a false sense of security. Operators may assume mutual TLS replaces application-layer authentication, but Django still validates usernames and passwords after the TLS handshake. If session cookies lack Secure and HttpOnly flags, or if HTTPS is not enforced consistently, intercepted session tokens can be reused. Moreover, mTLS does not protect against automated login attempts when the server issues a 200 OK for valid TLS but Django returns predictable responses that reveal whether a username exists, aiding iterative guessing. Therefore, monitoring and hardening must address both transport and identity verification layers.
Mutual Tls-Specific Remediation in Django — concrete code fixes
To securely integrate mutual TLS in Django, enforce client certificate validation at the web server or reverse proxy (e.g., Nginx or Apache) and ensure Django only receives requests with verified client identities. Configure Django to trust the proxy and use request attributes to identify users. Below are concrete code examples illustrating these steps.
1. Nginx configuration for mTLS
Require client certificates and validate them against a trusted CA, then forward the client certificate fingerprint or DN in a header that Django can use.
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/server.crt;
ssl_certificate_key /etc/ssl/private/server.key;
ssl_verify_client on;
ssl_client_certificate /etc/ssl/certs/ca.pem;
# Pass the client certificate fingerprint to Django
proxy_set_header SSL_CLIENT_CERT_SHA256 $ssl_client_s_dn_sha256;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
location / {
proxy_pass http://django_app;
proxy_set_header Host $host;
}
}
2. Django settings and middleware
Configure Django to trust headers set by the proxy and map the client identity to a user. Avoid using headers that can be spoofed without terminating TLS at the proxy.
# settings.py
import os
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Trust the proxy; ensure your proxy sets X-Forwarded-For and X-Forwarded-Proto correctly
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Custom authentication backend that uses the client certificate fingerprint
from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied
User = get_user_model()
class MTLSAuthenticationBackend:
def authenticate(self, request, ssl_client_cert_sha256=None):
if not ssl_client_cert_sha256:
return None
# Map fingerprint to a user; store mapping in a secure model
try:
user = User.objects.get(profile__cert_sha256=ssl_client_cert_sha256)
return user
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
3. Enforce rate limiting and secure session handling
Apply throttling in Django to limit requests per client identity and protect against brute force attempts. Use secure session cookies and ensure username enumeration is avoided.
# settings.py
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'path.to.MTLSTokenThrottle', # custom throttle keyed by client cert fingerprint
],
'DEFAULT_THROTTLE_RATES': {
'mtls_user': '5/minute', # adjust to your risk tolerance
}
}
# Example throttle class
from rest_framework.throttling import BaseThrottle
from django.core.cache import cache
class MTLSTokenThrottle(BaseThrottle):
def __init__(self):
self.rate = '5/minute'
def allow_request(self, request, view=None):
if request.META.get('HTTP_SSL_CLIENT_CERT_SHA256'):
key = f'mtls_{request.META["HTTP_SSL_CLIENT_CERT_SHA256"]}'
count = cache.get(key, 0)
if count >= 5:
return False
cache.set(key, count + 1, timeout=60)
return True
return False
4. Views and response hardening
Ensure views do not leak information about account existence and enforce HTTPS consistently. Use Django’s built-in protections alongside mTLS.
# views.py
from django.http import JsonResponse
from django.contrib.auth import authenticate, login
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def login_mtls_view(request):
cert_fp = request.META.get('HTTP_SSL_CLIENT_CERT_SHA256')
if not cert_fp:
return JsonResponse({'error': 'Client certificate required'}, status=400)
username = request.POST.get('username')
password = request.POST.get('password')
# Avoid user enumeration: attempt auth even if username is missing/invalid
user = MTLSAuthenticationBackend().authenticate(request, ssl_client_cert_sha256=cert_fp)
if user is None:
# Still attempt Django auth to not reveal username validity
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
return JsonResponse({'ok': True})
return JsonResponse({'error': 'Invalid credentials'}, status=401)