Broken Authentication in Django with Mutual Tls
Broken Authentication in Django with Mutual Tls — how this specific combination creates or exposes the vulnerability
Mutual Transport Layer Security (mTLS) requires both the client and the server to present valid certificates during the handshake. In Django, developers often assume mTLS alone is sufficient authentication, but mTLS handles transport identity, not application identity. If the application does not explicitly validate the client certificate against a trusted user or role mapping, authentication remains broken at the application layer.
Django’s built-in security does not map client certificates to users by default. A common pattern is to terminate TLS at a load balancer or reverse proxy (e.g., Nginx, HAProxy) and forward the certificate in a header such as SSL_CLIENT_CERT or HTTP_X_SSL_CLIENT_CERT. If Django trusts this header without strict validation, an attacker who can reach the application layer (e.g., via a misconfigured firewall or a compromised internal service) can spoof the header and authenticate as any identity the backend accepts.
Another vulnerability arises from insufficient certificate validation in the client verification path. For example, if Django accepts any client certificate signed by a trusted CA without checking the certificate’s subject, common name (CN), or extended key usage, an attacker can present any valid certificate and gain access. This is especially risky when authorization logic relies solely on the presence of a certificate rather than its attributes.
Consider a Django view that checks for a certificate but does not validate its fields:
import ssl
from django.http import JsonResponse
from django.views import View
class MTLSConsumerView(View):
def get(self, request):
cert = request.META.get('SSL_CLIENT_CERT')
if not cert:
return JsonResponse({'error': 'No client certificate'}, status=401)
# Vulnerable: no validation of cert fields
return JsonResponse({'authenticated': True})
In this example, any client that can reach the endpoint and present a valid certificate chain will be treated as authenticated, even if the certificate belongs to an unauthorized service or user. This misalignment between transport identity and application identity is the root cause of Broken Authentication in an mTLS-enabled Django application.
Additionally, if session cookies or token-based mechanisms are used alongside mTLS without proper binding, an attacker who steals a session token can impersonate a user even when mTLS is enforced. The combination of weak application-level identity checks and permissive trust in proxy headers leads to privilege escalation and unauthorized access.
Mutual Tls-Specific Remediation in Django — concrete code fixes
Remediation centers on validating client certificates in Django and binding them to application identities. You should validate the certificate’s subject, serial number, and extended key usage, and map it to an internal user or role before granting access.
Here is a secure example that extracts and validates the client certificate, then maps it to a Django user:
import ssl
from django.http import JsonResponse
from django.views import View
from django.contrib.auth.models import User
import OpenSSL.crypto
class SecureMTLSView(View):
def get(self, request):
cert_pem = request.META.get('SSL_CLIENT_CERT')
if not cert_pem:
return JsonResponse({'error': 'No client certificate'}, status=401)
try:
cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
except Exception:
return JsonResponse({'error': 'Invalid certificate'}, status=400)
# Validate certificate fields
subject = cert.get_subject()
common_name = subject.CN
if not common_name:
return JsonResponse({'error': 'Missing CN in certificate'}, status=403)
# Map CN to a Django user (example assumes CN matches username)
try:
user = User.objects.get(username=common_name)
except User.DoesNotExist:
return JsonResponse({'error': 'User not found'}, status=403)
# Ensure the certificate is within validity period (basic check)
not_before = cert.get_notBefore()
not_after = cert.get_notAfter()
# Implement proper datetime checks here as needed
request.user = user # Bind the authenticated user
return JsonResponse({'authenticated': True, 'user': user.username})
In this approach, the certificate is parsed using OpenSSL.crypto, the CN is extracted, and it is used to look up a Django user. Only after successful validation does the view proceed, ensuring the application identity matches the transport identity.
For production, enforce additional checks such as verifying certificate revocation via CRL or OCSP, validating the issuer against a pinned CA, and ensuring the key usage allows digital signatures. You can also store certificate fingerprints in the user model to establish a one-to-one mapping between certificates and accounts.
If you use a reverse proxy to terminate TLS, configure it to pass the client certificate fingerprint or serial number in a header, and validate those values in Django rather than trusting the proxy’s header alone. This prevents header spoofing from compromised internal networks.
Finally, combine mTLS with Django’s session framework or token-based authentication in a way that ties the certificate identity to the session, reducing the risk of token theft leading to unauthorized access.
Related CWEs: authentication
| CWE ID | Name | Severity |
|---|---|---|
| CWE-287 | Improper Authentication | CRITICAL |
| CWE-306 | Missing Authentication for Critical Function | CRITICAL |
| CWE-307 | Brute Force | HIGH |
| CWE-308 | Single-Factor Authentication | MEDIUM |
| CWE-309 | Use of Password System for Primary Authentication | MEDIUM |
| CWE-347 | Improper Verification of Cryptographic Signature | HIGH |
| CWE-384 | Session Fixation | HIGH |
| CWE-521 | Weak Password Requirements | MEDIUM |
| CWE-613 | Insufficient Session Expiration | MEDIUM |
| CWE-640 | Weak Password Recovery | HIGH |