HIGH cache poisoningdjangomutual tls

Cache Poisoning in Django with Mutual Tls

Cache Poisoning in Django with Mutual Tls — how this specific combination creates or exposes the vulnerability

Cache poisoning occurs when an attacker manipulates cached responses so that malicious content is served to other users. In Django, when mutual TLS (mTLS) is used for client authentication, the combination of per‑client caching and trust in authenticated client identities can inadvertently enable cache key collisions that lead to poisoning.

Django’s HTTP cache (e.g., Cache-Control and Vary) can key cache entries by headers such as Authorization or custom headers like X-Client-Cert-Serial. With mTLS, the server validates the client certificate, but if the cache key includes only a subset of request attributes (e.g., path + a single header) and the response contains user-specific or tenant-specific data, one client’s cached response can be reused for another. For example, if the cache key omits the full certificate identity but includes an Accept-Language header, a poisoned response crafted for one language/certificate pair may be served to others who happen to match the partial key.

Additionally, mTLS setups often involve intermediaries (load balancers or ingress controllers) that terminate TLS. If Django’s SECURE_PROXY_SSL_HEADER is misconfigured while mTLS verification still occurs at the application layer, mismatched trust boundaries can cause Django to treat different authenticated identities as equivalent, leading to incorrect cache lookups. Sensitive data from a high-privilege certificate may be cached under a key that does not include the full certificate fingerprint, and subsequent requests from lower-privilege clients may receive that cached data.

Real-world attack patterns include exploiting the Vary header to inject variant responses, or leveraging inconsistent cache keys to perform privilege escalation via stored responses (a form of BOLA/IDOR). This intersects with mTLS because the authentication signal (client cert) is used for access control but not consistently for cache isolation, violating the principle that cache keys should incorporate all dimensions that affect response content and security context.

Consider a scenario where Django uses django.middleware.cache.UpdateCacheMiddleware and FetchFromCacheMiddleware with a Vary: Authorization header, but the certificate’s serial number is not part of the key. An attacker with a valid client certificate could craft requests that cause responses containing private data to be cached under a key that does not uniquely identify the certificate, enabling cross-client data leakage.

Mutual Tls-Specific Remediation in Django — concrete code fixes

To mitigate cache poisoning with mTLS in Django, ensure cache keys incorporate the full security context provided by the client certificate. Use certificate-derived identifiers (e.g., serial, fingerprint, or subject hash) in the cache key and configure Vary appropriately. Avoid relying solely on headers that may be stripped or overridden by intermediaries.

mTLS-aware cache key construction

Create a custom cache key function that includes the client certificate fingerprint. This example uses the SSL_CLIENT_S_DN_Email or a custom header injected by the proxy after mTLS verification:

import hashlib
def get_cache_key(request):
    # Assume the proxy sets X-Client-Cert-SHA256 after validating mTLS
    cert_fingerprint = request.META.get('HTTP_X_CLIENT_CERT_SHA256', '')
    path = request.get_full_path()
    method = request.method
    # Include fingerprint in the key to isolate per-client caches
    key_parts = [method, path, cert_fingerprint]
    return hashlib.sha256(':'.join(key_parts).encode()).hexdigest()

Then in your view or decorator:

from django.views.decorators.cache import cache_page
def my_view(request):
    key = get_cache_key(request)
    # Use a cache alias configured for mTLS-aware keys
    return cache_page(lambda r: HttpResponse('secure'), cache=get_cache('secure_cache'), key_prefix=key)(request)

Django settings for mTLS and cache integrity

Configure Django to trust the proxy only when mTLS has been verified, and ensure Vary includes the certificate-derived identifier:

# settings.py
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Do NOT rely on this alone for mTLS; validate certs in the view or middleware

# Custom cache backend that respects per-client isolation
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
        },
    },
    'secure_cache': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
        'LOCATION': 'secure-cache',
    }
}

Add a middleware that enforces mTLS and sets a request attribute for cache key construction:

import ssl
class MutualTlsCacheMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # Example: ensure client cert is present and valid
        cert = request.META.get('SSL_CLIENT_CERT')
        if not cert:
            from django.http import HttpResponseForbidden
            return HttpResponseForbidden('Client certificate required')
        # Compute and attach a safe identifier for caching
        fingerprint = ssl.DER_cert_to_PEM_cert(cert)  # simplified; use cryptography in prod
        import hashlib
        request.client_cert_fingerprint = hashlib.sha256(fingerprint.encode()).hexdigest()
        response = self.get_response(request)
        response['Vary'] = ', '.join(['Authorization', 'X-Client-Cert-SHA256'])
        return response

In views that handle sensitive data, explicitly vary by the certificate fingerprint and avoid caching responses with user-specific payloads unless the key fully isolates them:

from django.views.decorators.vary import vary_on_headers
@cache_page(60 * 15)
@vary_on_headers('X-Client-Cert-SHA256', 'Authorization')
def sensitive_data(request):
    # Your logic here; cache key includes certificate fingerprint
    return JsonResponse({'data': 'private'})

These steps align cache behavior with mTLS identities, reducing the risk that a response intended for one authenticated client is mistakenly cached and served to another.

Frequently Asked Questions

How does mutual TLS change cache key design compared to traditional authentication?
With mTLS, the client identity is established by the certificate presented during the TLS handshake rather than a bearer token. Cache keys must incorporate a stable, tamper-evident identifier derived from the certificate (e.g., fingerprint or serial) to prevent one client’s cached response from being reused by another. This differs from token-based auth where the token is often part of the Authorization header and can be explicitly included in cache keys; with mTLS, you must explicitly extract and include the cert-derived identifier in the key and Vary.
Can middleware that validates mTLS alone prevent cache poisoning?
No. Validating the client certificate is necessary but insufficient if the cache layer does not use the certificate identity as part of the cache key. Middleware can enforce mTLS and attach a fingerprint to the request, but you must ensure views and cache decorators incorporate that fingerprint into the key and that Vary headers reflect the certificate-derived identifier to avoid cross-client cache collisions.