Prototype Pollution in Django with Mutual Tls
Prototype Pollution in Django with Mutual Tls — how this specific combination creates or exposes the vulnerability
Prototype pollution in Django occurs when an attacker can modify the prototype of objects used by the application, often through crafted input that is merged into shared objects such as request.GET, request.POST, or deserialized data structures. In a typical Django view, nested keys from JSON or query parameters are sometimes merged into a base dictionary without validation, allowing an attacker to set or overwrite existing object properties (e.g., __proto__, constructor, or other inherited properties). When mutual TLS (mTLS) is in use, the server authenticates the client by verifying the client certificate before the application code sees the request. This can create a false sense of security: mTLS ensures the client is trusted, but it does not sanitize or validate the content of the request. If a Django service behind mTLS accepts JSON or form data from that authenticated client and merges it into mutable objects, the trusted channel does not prevent prototype pollution. Moreover, mTLS setups are common in microservice and API-to-API contexts where Django acts as a consumer or gateway; if the upstream service assumes the downstream is secured by mTLS and skips input validation, the attack surface expands. An attacker who compromises a client certificate or shares a weak certificate policy may send maliciously crafted payloads that pollute prototypes, potentially affecting configuration, deserialization paths, or behavior of subsequent processing. For example, if Django uses request.data from DRF and merges it into a module-level defaults dictionary, an attacker could set {"__proto__": {"is_admin": true}} and influence object behavior across requests. The risk is not that mTLS introduces the vulnerability, but that it may relax other validation assumptions, making prototype pollution more likely to reach production code paths.
Mutual Tls-Specific Remediation in Django — concrete code fixes
Protecting Django applications when mutual TLS is used requires strict input validation and isolation between transport-layer identity and application-level data handling. Treat authenticated client certificates as identity, not as authorization to trust request content. Follow these concrete practices and code examples.
1. Enforce mTLS at the load balancer or reverse proxy
Let the edge terminate TLS and set a normalized header (e.g., X-Client-Cert) containing the certificate fingerprint. Keep Django’s SECURE_PROXY_SSL_HEADER consistent and avoid relying on request.is_secure() alone. This separation ensures Django sees the request as secure while still validating data independently.
2. Validate and sanitize all incoming data
Never merge raw input into shared or module-level objects. In Django REST Framework, use serializers with strict fields and avoid partial=True merges that rely on request.data directly for prototype-sensitive operations. Use explicit fields and to_internal_value to control merging.
from rest_framework import serializers
class SafeConfigSerializer(serializers.Serializer):
setting_a = serializers.CharField(required=False)
setting_b = serializers.IntegerField(required=False)
def to_internal_value(self, data):
# Explicitly pick allowed keys; ignore __proto__ and other dangerous keys
allowed = {"setting_a", "setting_b"}
filtered = {k: v for k, v in data.items() if k in allowed}
return super().to_internal_value(filtered)
3. Avoid module-level mutable defaults
Do not use request data to update module-level dictionaries or singletons. Instead, create fresh objects per request or use thread-safe, isolated storage. Example of a vulnerable pattern and a safe alternative.
Vulnerable:
defaults = {"timeout": 30, "retries": 3}
def get_config(updates):
# Dangerous: mutates shared prototype
merged = defaults
merged.update(updates)
return merged
Safe:
from copy import deepcopy
def get_config(updates):
config = deepcopy(defaults)
config.update(updates)
return config
4. Use Django’s built-in validation and CSRF protections
Even with mTLS, include CsrfViewMiddleware for state-changing views and use ensure_csrf_cookie where needed. Validate content types and reject unexpected payloads.
5. Example mTLS-enabled Django view with strict parsing
import ssl
from django.conf import settings
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_protect
from .serializers import SafeConfigSerializer
# Assume X-Client-Cert is set by proxy
def client_cert_info(request):
cert = request.META.get("HTTP_X_CLIENT_CERT")
return {"cert_fingerprint": cert}
@csrf_protect
def secure_config_view(request):
if request.method != "POST":
return JsonResponse({"error": "method not allowed"}, status=405)
# Validate content type
if not request.content_type == "application/json":
return JsonResponse({"error": "unsupported media type"}, status=415)
serializer = SafeConfigSerializer(data=request.data)
if not serializer.is_valid():
return JsonResponse({"errors": serializer.errors}, status=400)
config = get_config(serializer.validated_data)
return JsonResponse({"config": config, "client": client_cert_info(request)})
6. Middleware to reject dangerous keys
Add lightweight middleware to strip or reject keys like __proto__, constructor, and prototype before they reach views.
class PrototypePollutionProtectionMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.pollution_keys = {"__proto__", "constructor", "prototype"}
def __call__(self, request):
if hasattr(request, "data") and isinstance(request.data, dict):
for key in list(request.data.keys()):
if key in self.pollution_keys:
del request.data[key]
return self.get_response(request)
7. Regular certificate and policy review
Audit certificate issuance policies, enforce strong CA constraints, and rotate certificates. Combine mTLS with short-lived certs and revocation (OCSP/CRL) to reduce the impact of compromised credentials.