Ssrf Server Side in Django with Mutual Tls
Ssrf Server Side in Django with Mutual Tls — how this specific combination creates or exposes the vulnerability
Server-side request forgery (SSRF) in Django can intersect with mutual TLS (mTLS) in nuanced ways. When Django acts as a client and makes outbound requests to upstream services, developers sometimes enforce mTLS to strengthen authentication and encryption. This often involves loading a client certificate and key (and optionally a trusted CA bundle) into the HTTP client. However, introducing mTLS does not automatically prevent SSRF; it can inadvertently change the attack surface. An attacker who can influence the target URL may still force Django to initiate requests to arbitrary destinations. If the Django code uses the same client certificate and key for all outbound calls, an SSRF victim may be any server that presents a certificate trusted by the CA used to sign the malicious server’s certificate, or that accepts the provided client certificate. In some configurations, mTLS can also cause Django applications to trust internal or private endpoints more implicitly, leading to unintended lateral movement. Moreover, if the certificate or key paths are exposed through logs or error messages, they may become sensitive data exfiltration channels. SSRF tests in middleBrick’s Unauthenticated LLM endpoint and Data Exposure checks can surface these risks by probing how Django-based APIs handle manipulated destinations and inspecting whether sensitive material appears in responses.
Mutual Tls-Specific Remediation in Django — concrete code fixes
Remediation focuses on strict URL allowlisting, network segregation, and careful handling of mTLS artifacts. Avoid using a single client certificate for all destinations, and do not trust internal hostnames implicitly. Use an allowlist of permitted hosts and ports, and prefer short-lived certificates scoped to specific services. If you must use mTLS, isolate outbound calls to known services and do not propagate client certificates to dynamically constructed URLs.
Example: Unsafe Django view with SSRF risk using requests and mTLS files
import requests
from django.http import JsonResponse
def unsafe_proxy(request):
target = request.GET.get("url")
# WARNING: attacker-controlled target
cert = ("/path/client.crt", "/path/client.key")
ca_bundle = "/path/ca-bundle.crt"
resp = requests.get(target, cert=cert, verify=ca_bundle, timeout=5)
return JsonResponse({"status": resp.status_code, "body": resp.text[:200]})
Example: Safer Django view with explicit allowlist and scoped mTLS
import requests
from django.http import JsonResponse, HttpResponseBadRequest
ALLOWED_HOSTS = {"api.example.com": 443, "internal.example.com": 8443}
def safe_proxy(request):
target = request.GET.get("url")
if not target:
return HttpResponseBadRequest("Missing url parameter")
from urllib.parse import urlparse
parsed = urlparse(target)
netloc = parsed.netloc.split(":")[0] # strip port for hostname check
port = parsed.port or (443 if parsed.scheme == "https" else 80)
if parsed.scheme not in ("https",) or (netloc, port) not in ALLOWED_HOSTS.items():
return HttpResponseBadRequest("Destination not allowed")
# Use per-service credentials or outbound network policies instead of a shared cert
# Example: restrict to a dedicated CA and no client cert for this endpoint
ca_bundle = "/path/ca-bundle.crt"
try:
resp = requests.get(target, verify=ca_bundle, timeout=5)
resp.raise_for_status()
except requests.RequestException as e:
return JsonResponse({"error": str(e)}, status=502)
return JsonResponse({"status": resp.status_code, "body": resp.text[:200]})
Example: mTLS with requests and certificate binding (when required by policy)
import requests
from django.http import JsonResponse
from urllib.parse import urlparse
# Map allowed hostnames to their specific cert/key pairs
CERT_MAP = {
"api.example.com": ("/certs/api_client.crt", "/certs/api_client.key"),
"internal.example.com": ("/certs/internal_client.crt", "/certs/internal_client.key"),
}
ALLOWED_HOSTS = set(CERT_MAP.keys())
def mtlscert_proxy(request):
target = request.GET.get("url")
if not target:
return JsonResponse({"error": "Missing url parameter"}, status=400)
parsed = urlparse(target)
if parsed.scheme != "https" or parsed.netloc.split(":")[0] not in ALLOWED_HOSTS:
return JsonResponse({"error": "Destination not allowed"}, status=403)
cert = CERT_MAP[parsed.netloc.split(":")[0]]
ca_bundle = "/certs/ca-bundle.crt"
try:
resp = requests.get(target, cert=cert, verify=ca_bundle, timeout=5)
resp.raise_for_status()
except requests.RequestException as e:
return JsonResponse({"error": str(e)}, status=502)
return JsonResponse({"status": resp.status_code})
Operational and architectural recommendations
- Do not place sensitive certificates or keys in application source or version control; use environment variables or a secrets manager and restrict file permissions.
- Restrict outbound network access at the container or host level so Django can only reach approved endpoints (zero-trust network policies).
- Log connection attempts without logging certificate material; ensure logs do not inadvertently capture private keys or PII.
- In CI/CD, include middleBrick’s GitHub Action to fail builds if new endpoints are introduced without allowlist entries or if mTLS artifacts are mishandled.
- For development and AI-assisted workflows, use the MCP Server to scan APIs directly from your IDE and surface SSRF and mTLS misconfigurations early.