Token Leakage in Django with Mutual Tls
Token Leakage in Django with Mutual TLS — how this specific combination creates or exposes the vulnerability
Token leakage in a Django service that uses mutual TLS (mTLS) often occurs when developers treat mTLS as the sole authentication mechanism and inadvertently expose session or API tokens over the same channel. mTLS provides strong client authentication via client certificates, but it does not automatically prevent token exposure in application code, logs, error messages, or downstream proxies. When tokens are included in URLs, query parameters, or unencrypted HTTP headers, they can be captured in server logs, access logs, or by intermediary proxies that terminate TLS but still communicate internally over HTTP.
In a Django deployment with mTLS, the web server (e.g., Nginx or Envoy) is typically configured to require and validate client certificates before forwarding requests to Django. If the Django application then generates or handles tokens (e.g., OAuth2 access tokens, session tokens, or API keys) and includes them in responses, logs, or URLs, those tokens can be leaked to unauthorized parties who have access to logs or who can observe traffic between the mTLS-terminating proxy and Django (especially if that segment is not encrypted or is misconfigured). Additionally, misconfigured certificate verification or incomplete chain validation can allow unauthorized clients to connect, increasing the risk that tokens are presented or logged for unauthenticated or unexpected peers.
Another vector specific to mTLS-enabled Django apps is the reliance on the proxy to provide trustworthy client identity. If Django uses request.META variables populated by the proxy (such as SSL_CLIENT_VERIFY or SSL_CLIENT_CERT) without additional validation, it may trust headers injected by a misconfigured or compromised proxy. An attacker who can control or observe upstream traffic might inject tokens into headers that Django then logs or echoes. For example, a token passed in a custom header like X-API-Key might be logged in full request logs if the logging configuration captures headers indiscriminately, exposing secrets alongside the mTLS certificate metadata.
Real-world attack patterns such as log injection, insufficient transport layer protection between proxy and application, and improper header handling are described in the OWASP API Security Top 10 and can map to findings in the Data Exposure and Authentication checks run by middleBrick. The scanner tests whether tokens appear in URLs, error responses, and logs, and whether transport protections are consistent across the full path from client to Django. Even with mTLS, if tokens are present in plaintext in HTTP messages or stored insecurely in Django’s session store or cookies without the Secure and HttpOnly flags, the risk of leakage remains high.
Compliance frameworks such as OWASP API Top 10 (2023) — especially Security Misconfiguration and Excessive Data Exposure — and standards like PCI-DSS and SOC2 highlight the need to protect authentication materials at every layer. middleBrick’s LLM/AI Security checks do not apply here, but its Authentication, Data Exposure, and Inventory Management checks help identify token leakage by correlating runtime findings with OpenAPI specifications and runtime behavior. By scanning unauthenticated attack surfaces and resolving spec definitions against observed responses, middleBrick surfaces exposed tokens and highlights missing transport protections or unsafe logging practices so teams can remediate the underlying configuration and code issues.
Mutual TLS-Specific Remediation in Django — concrete code fixes
Remediation focuses on ensuring mTLS is correctly configured at the proxy or web server, hardening Django’s handling of client identity, and preventing tokens from being logged or exposed. Below are concrete steps and code examples for a typical setup where Nginx terminates mTLS and forwards requests to Django over localhost HTTP (with encryption maintained in the broader architecture).
1. Nginx mTLS configuration (example)
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/server.crt;
ssl_certificate_key /etc/ssl/private/server.key;
# Require and verify client certificates
ssl_verify_client on;
ssl_client_certificate /etc/ssl/certs/ca-chain.pem;
# Custom header with verified client certificate fingerprint (do not forward raw certs)
proxy_set_header X-Client-Verify $ssl_client_verify;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-Client-Fingerprint $ssl_client_fingerprint;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Do not forward client certificate; use fingerprint or DN
}
}
2. Django settings and middleware adjustments
In Django settings, avoid trusting arbitrary headers for security decisions. Instead, map verified proxy headers to request attributes in a custom middleware:
import ssl
from django.utils.deprecation import MiddlewareMixin
class MtlsClientInfoMiddleware(MiddlewareMixin):
"""
Map verified proxy headers to a normalized attribute.
Ensure these headers are only set by the trusted proxy (e.g., Nginx).
"""
def process_request(self, request):
verify = request.META.get('HTTP_X_CLIENT_VERIFY', '').upper()
if verify == 'SUCCESS':
request.client_dn = request.META.get('HTTP_X_CLIENT_DN', '')
request.client_fingerprint = request.META.get('HTTP_X_CLIENT_FINGERPRINT', '')
# Do NOT assign raw client certificate contents
else:
request.client_dn = None
request.client_fingerprint = None
3. Securing tokens in views and responses
Ensure tokens are never rendered in URLs or query parameters. Use POST bodies or Authorization headers with Bearer tokens, and enforce HTTPS site-wide. Set Secure and HttpOnly on cookies, and avoid logging request headers that may contain tokens:
from django.conf import settings
from django.shortcuts import jsonify
import logging
logger = logging.getLogger(__name__)
def my_view(request):
# Use request.META populated by middleware, not raw headers
client_dn = getattr(request, 'client_dn', None)
if not client_dn:
return jsonify({'error': 'client certificate not verified'}), 403
# Example: issue a scoped token without echoing secrets in logs
token = generate_scoped_token(client_dn)
logger.info('Token issued for client_dn=%s', client_dn, extra={
'headers': {k: v for k, v in request.headers.items() if k.lower() != 'authorization'}
})
return jsonify({'token': token})
4. Logging and data exposure controls
Configure logging to exclude sensitive headers and body content. In Django settings:
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'redact_headers': {
'()': 'django.utils.log.CallbackFilter',
'callback': lambda record: not any(h in record.getMessage() for h in ('authorization', 'x-api-key', 'cookie'))
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'filters': ['redact_headers']
}
},
'loggers': {
'django.request': {
'handlers': ['console'],
'level': 'WARNING',
},
},
}
5. Continuous monitoring and scanning
Use the middleBrick CLI to scan your API endpoints regularly and validate that tokens are not exposed in responses or logs:
middlebrick scan https://api.example.com/openapi.json
For automated checks in pipelines, add the GitHub Action to fail builds when risk scores degrade, and consider the Pro plan for continuous monitoring and PR gates. The MCP Server lets you scan directly from your IDE when developing new endpoints that issue or handle tokens.