Integrity Failures in Django with Mutual Tls
Integrity Failures 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 and validate certificates. In Django, developers often terminate TLS at a load balancer or reverse proxy and then forward requests over an internal, unencrypted channel to the Django application. When mTLS is misconfigured or assumed to be enforced at the Django layer without explicit enforcement, integrity failures arise because Django cannot trust that the connection was authenticated by a valid client certificate.
An integrity failure occurs when Django processes a request without verifying that the client certificate was validated. For example, if you rely on the presence of SSL_CLIENT_VERIFY set by your proxy but do not enforce certificate checks in Django middleware, an attacker who can reach Django directly (bypassing the proxy) or spoof headers can act as an authenticated client. This violates integrity because the assurance that the client is who they claim to be is broken.
One realistic scenario involves using mTLS to authorize access to an internal API endpoint. A proxy validates the client certificate and forwards the request to Django with a header like X-SSL-Client-Subject. If Django uses this header to determine permissions without re-verifying the certificate chain, it trusts a value that can be forged. This maps to common Web Security Testing categories such as BOLA/IDOR and Authentication when the trust boundary is not clearly defined.
Django’s default settings do not enforce client certificates; they only provide access to SSL-related request attributes when the WSGI server populates them. If you configure Django to require HTTPS via SECURE_SSL_REDIRECT = True but do not validate mTLS at the application or proxy layer, you create a gap between transport security and identity integrity.
Consider an endpoint that returns sensitive project data. With a misconfigured mTLS setup, an unauthenticated network position that can reach Django might supply the expected headers and retrieve data, because Django lacks a runtime check that a valid client certificate was verified. This is why it is critical to treat mTLS enforcement as a cross-cutting concern: either enforce it at the proxy and ensure Django only runs behind that proxy, or implement explicit certificate validation in Django middleware.
Mutual Tls-Specific Remediation in Django — concrete code fixes
To remediate integrity failures, enforce mTLS at the point where Django receives requests and validate client certificates explicitly when necessary. Below are concrete patterns you can apply depending on where TLS termination occurs.
Option 1: Enforce mTLS at the proxy and ensure Django only runs behind it. Configure your load balancer or reverse proxy (e.g., Nginx) to require and validate client certificates, and only forward requests to Django if the certificate is valid. In Django, you should still reject requests that lack proof of successful mTLS by inspecting a trusted header set by the proxy:
import re
from django.http import HttpResponseForbidden
TRUSTED_PROXY_SUBJECTS = [
r'/CN=client-a\.corp\.example\.com/',
r'/CN=client-b\.corp\.example\.com/',
]
class MutualTlsMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
ssl_client_subject = request.META.get('HTTP_X_SSL_CLIENT_SUBJECT', '')
if not ssl_client_subject:
return HttpResponseForbidden('Missing client certificate subject')
if not any(re.match(pattern, ssl_client_subject) for pattern in TRUSTED_PROXY_SUBJECTS):
return HttpResponseForbidden('Untrusted client certificate')
response = self.get_response(request)
return response
Register this middleware early in MIDDLEWARE so it runs before your view logic.
Option 2: Validate client certificates directly in Django when terminating TLS at the app. If Django terminates TLS, configure your WSGI server (e.g., Gunicorn) to provide the certificate and use application-level validation. With Gunicorn, you can provide --ssl-certfile and --ssl-keyfile along with --ca-certs to enable mTLS, and then inspect the request’s peer certificate in Django:
import ssl
from django.http import HttpResponseForbidden
def verify_client_cert(get_response):
def middleware(request):
# When terminating TLS in Django, the certificate may be available via request attributes
# set by the WSGI server. Example using gunicorn ssl_wrap_socket.
ssl_attrs = getattr(request, 'ssl', None)
if ssl_attrs is None:
return HttpResponseForbidden('No SSL information')
client_cert = getattr(ssl_attrs, 'client_cert', None)
if client_cert is None:
return HttpResponseForbidden('Missing client certificate')
# Perform additional checks: verify issuer, expiry, CN/SAN mapping
# This is a simplified example; use cryptography to parse the PEM.
cert_pem = client_cert.public_bytes(encoding=ssl.Encoding.PEM)
# Validate against allowed issuers or CRLs here
response = get_response(request)
return response
return middleware
Proxy header validation pattern. When a trusted proxy terminates mTLS, ensure you validate and sanitize headers before using them in permissions:
from django.conf import settings
def get_trusted_subjects():
# In production, load from a secure configuration or environment variable
return getattr(settings, 'TRUSTED_PROXY_CLIENT_SUBJECTS', [])
def require_mtls(view_func):
def wrapped(request, *args, **kwargs):
subject_dn = request.META.get('HTTP_X_SSL_CLIENT_SUBJECT')
if not subject_dn:
from django.http import HttpResponseForbidden
return HttpResponseForbidden('Client certificate required')
allowed = get_trusted_subjects()
if not any(subject_dn.startswith(a) or re.match(a, subject_dn) for a in allowed):
from django.http import HttpResponseForbidden
return HttpResponseForbidden('Unauthorized client')
return view_func(request, *args, **kwargs)
return wrapped
Map these findings to compliance frameworks such as OWASP API Top 10 (2023) — for example, broken object level authorization and improper authentication controls — and reference real CVEs where similar misconfigurations led to privilege escalation or data exposure.