Vulnerable Components in Django with Mutual Tls
Vulnerable Components in Django with Mutual Tls — how this specific combination creates or exposes the vulnerability
Mutual Transport Layer Security (mTLS) in Django means both the client and the server present certificates during the TLS handshake. When mTLS is added to Django, new components come into scope and misconfigurations can reintroduce familiar risks such as Insecure Transport, Weak Certificate Validation, and Improper Access Control. These issues map directly to the OWASP API Top 10 and can shift the security risk score assessed by middleBrick.
One common vulnerable pattern is relying solely on mTLS for authentication while neglecting per-request authorization. For example, a view may verify that a client certificate was accepted by the server, but then fail to enforce object-level permissions or scope checks. This can lead to Broken Object Level Authorization (BOLA/IDOR), where one authenticated client can access or modify another client’s data. middleBrick’s BOLA/IDOR checks are designed to detect these authorization gaps even in unauthenticated scans.
Another vulnerability vector is incomplete certificate validation. Developers sometimes set SSL_CLIENT_VERIFY checks to optional or none in Django’s SSL settings, or they skip hostname verification when validating client certificates. An attacker could then present a valid but misissued certificate from a trusted CA or attempt to downgrade the handshake. Input validation checks in middleBrick flag weak certificate usage and missing hostname verification, highlighting cases where the server accepts connections without properly confirming the client identity.
Improper configuration of the WSGIHandler or the underlying web server (e.g., Gunicorn, uWSGI) can also expose sensitive information. If HTTP headers like SSL_CLIENT_S_DN or certificate fields are logged without sanitization, they might disclose distinguished names or serial numbers that aid reconnaissance. Data exposure checks in middleBrick look for excessive information in responses and logs, which can be critical when mTLS metadata is involved.
Finally, missing or misconfigured rate limiting becomes more dangerous with mTLS because authenticated client certificates can give a false sense of strong identity. Without rate limiting, an attacker who obtains a valid client certificate can perform credential stuffing-like attacks or abuse high-cost endpoints. middleBrick’s rate limiting tests evaluate whether endpoints properly throttle requests, regardless of whether mTLS is in place.
Mutual Tls-Specific Remediation in Django — concrete code fixes
To securely implement mTLS in Django, combine proper SSL settings with explicit certificate validation and robust authorization. Below are concrete, realistic code examples you can apply.
1. Configure Django to require and validate client certificates
In your Django settings, enforce client certificates and validate them against a trusted CA. Avoid permissive settings.
import os
# settings.py
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Require client certificates
SSL_CLIENT_VERIFY = 'optional' # Use 'require' in production after confirming clients send certs
# Better: validate manually in middleware to have full control
# Path to trusted CA bundle used to verify client certificates
SSL_CA_FILE = os.path.join(BASE_DIR, 'certs', 'ca-bundle.crt')
# Optional: restrict to specific subject fields if your PKI policy requires it
SSL_CLIENT_REQUIRED_SUBJECT = 'CN=clients,O=Example Org,C=US'
2. Middleware to enforce authorization after mTLS authentication
Even when a client certificate is validated, enforce object-level permissions on each request.
# middleware.py
import json
from django.http import HttpResponseForbidden
class MutualTlsAuthorizationMiddleware:
def __init__(self get_response):
self.get_response = get_response
def __call__(self request):
# Assuming client identity is mapped in request by a prior step
client_dn = request.META.get('SSL_CLIENT_S_DN')
if not client_dn:
return HttpResponseForbidden('Client certificate required')
# Example: map DN to a Django user and enforce ownership
user = self.map_dn_to_user(client_dn)
if not user or not user.is_active:
return HttpResponseForbidden('Invalid or inactive client')
request.user = user
response = self.get_response(request)
return response
def map_dn_to_user(self, dn):
# Implement your mapping logic, e.g., via a ClientCertificate model
from .models import ClientCertificate
try:
cert = ClientCertificate.objects.get(dn=dn)
return cert.user
except ClientCertificate.DoesNotExist:
return None
3. Validate certificate fields and hostname in custom checks
Do not rely on the web server’s automatic validation alone. Add explicit checks for certificate validity and hostname alignment.
# utils.py
import ssl
from OpenSSL import crypto
def validate_client_cert(cert_pem, expected_hostname):
cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
store = ssl.X509Store()
# Load trusted CA
with open('certs/ca-bundle.crt', 'rb') as f:
ca_cert = ssl.DER_cert_to_PEM_cert(f.read())
store.add_cert(ssl.load_certificate(ssl.FILETYPE_PEM, ca_cert))
store_ctx = ssl.X509StoreContext(store, cert)
try:
store_ctx.verify_certificate()
except ssl.SSLException:
raise ValueError('Certificate verification failed')
# Check SAN or CN for expected hostname (simplified example)
subject = cert.get_subject()
common_name = subject.CN
if common_name != expected_hostname:
raise ValueError('Hostname mismatch')
return cert
4. Apply per-request authorization and scope checks
After authentication, ensure each endpoint verifies that the client is allowed to access the requested resource.
# views.py
from django.http import JsonResponse, HttpResponseForbidden
from .models import Dataset
def dataset_detail(request, dataset_id):
client_dn = request.META.get('SSL_CLIENT_S_DN')
if not client_dn:
return HttpResponseForbidden('Certificate required')
dataset = Dataset.objects.filter(id=dataset_id).first()
if not dataset:
return JsonResponse({'error': 'Not found'}, status=404)
# Enforce ownership or shared access rules
if not request.user.can_access(dataset):
return HttpResponseForbidden('Access denied')
return JsonResponse({'id': dataset.id, 'name': dataset.name})
5. Secure logging and avoid leaking certificate details
Ensure certificate fields are not inadvertently exposed in logs or error messages that could aid an attacker.
# settings.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.request': {
'handlers': ['console'],
'level': 'WARNING',
'filters': [],
'propagate': True,
},
},
}
# Avoid logging raw SSL_CLIENT_S_DN; sanitize if needed
import logging
logger = logging.getLogger(__name__)
def safe_log_client_info(request):
dn = request.META.get('SSL_CLIENT_S_DN')
if dn:
# Hash or truncate sensitive fields before logging
logger.info('Request received with authenticated client')
else:
logger.warning('Request without client certificate')
6. Combine mTLS with rate limiting and monitoring
Use Django middleware or an external layer to limit requests per client certificate to mitigate abuse.
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'yourapp.middleware.MutualTlsAuthorizationMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django_ratelimit.middleware.RatelimitMiddleware',
]
# views.py example using ratelimit
from ratelimit.decorators import ratelimit
@ratelimit(key='header:SSL_CLIENT_SERIAL', rate='100/m', block=True)
def sensitive_operation(request):
return JsonResponse({'status': 'ok'})