HIGH ssrfdjangomutual tls

Ssrf in Django with Mutual Tls

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

Server-Side Request Forgery (SSRF) in Django becomes particularly nuanced when Mutual TLS (mTLS) is in use. mTLS requires both the client and the server to present valid certificates during the TLS handshake. In Django, developers often use requests or httpx to call downstream services and may configure mTLS by providing cafile, certfile, and keyfile paths. When an attacker can influence the target URL of an HTTP request made by the server, they can direct the server to connect to internal endpoints that are not intended to be exposed. Even with mTLS protecting inbound traffic to your Django app, an SSRF against an outbound mTLS-enabled service can bypass network-level segregation because the Django process itself holds a valid client certificate and private key.

Consider a scenario where a Django view accepts a URL parameter to fetch metadata from a partner service that enforces mTLS:

import requests
from django.http import JsonResponse
from django.views.decorators.http import require_GET

@require_GET
def fetch_service_metadata(request):
    url = request.GET.get('url')
    # WARNING: user-controlled URL with mTLS client certs
    resp = requests.get(
        url,
        cert=('client.crt', 'client.key'),
        verify='ca-bundle.crt'
    )
    return JsonResponse({'status': resp.status_code, 'text': resp.text[:200]})

If the attacker provides a URL pointing to an internal mTLS-protected admin endpoint (e.g., https://internal-service:8443/admin/debug), the Django server will successfully authenticate using its client certificate and relay the response, effectively pivoting through mTLS authentication. The presence of mTLS may create a false sense of security: it secures server-to-server communication but does not prevent the Django app from being used as a proxy to internal mTLS-protected systems. Additional risks emerge when the downstream service trusts the client certificate presented by Django, allowing access to restricted resources. Moreover, certificate validation misconfigurations (e.g., verify=False or improper ca bundles) can compound the issue, enabling man-in-the-middle scenarios or bypassing intended trust boundaries.

middleBrick identifies such patterns by correling OpenAPI specifications with runtime behavior, including mTLS configurations where client certificates are referenced. This helps highlight how an SSRF vector can leverage trusted identities in mTLS workflows, emphasizing the need to treat outbound requests with the same scrutiny as inbound protections.

Mutual Tls-Specific Remediation in Django — concrete code fixes

Remediation centers on input validation, network segregation, and minimizing the trust surface of client certificates. Avoid forwarding user-supplied URLs directly to services that use mTLS. If you must allow dynamic endpoints, enforce a strict allowlist of hostnames and paths, and use a dedicated outbound mTLS context that does not expose elevated privileges.

1) Validate and restrict target hosts

Instead of passing user input to requests, map allowed services to a whitelist and construct the request safely:

from urllib.parse import urlparse
import requests
from django.http import JsonResponse
from django.views.decorators.http import require_GET

ALLOWED_HOSTS = {'api.partner.com', 'data.example.org'}

@require_GET
def fetch_service_metadata_safe(request):
    url = request.GET.get('url')
    parsed = urlparse(url)
    if parsed.hostname not in ALLOWED_HOSTS:
        return JsonResponse({'error': 'host not allowed'}, status=400)
    # Use a fixed client certificate scoped for outbound calls only
    resp = requests.get(
        url,
        cert=('outbound_client.crt', 'outbound_client.key'),
        verify='ca-bundle-partner.crt'
    )
    return JsonResponse({'status': resp.status_code, 'text': resp.text[:200]})

2) Use a dedicated outbound TLS context and avoid certificate propagation

Do not reuse the same client certificate used for inbound mTLS. Create a separate certificate with minimal privileges for outbound calls:

import requests
from django.http import JsonResponse
from django.views.decorators.http import require_GET

@require_GET
def fetch_with_isolated_mtls(request):
    url = request.GET.get('url')
    parsed = urlparse(url)
    if parsed.hostname not in {'api.partner.com'}:
        return JsonResponse({'error': 'host not allowed'}, status=400)
    # Dedicated certificate scoped for this specific partner
    resp = requests.get(
        url,
        cert=('outbound_partner.crt', 'outbound_partner.key'),
        verify='/etc/ssl/certs/partner-ca.crt'
    )
    return JsonResponse({'status': resp.status_code})

3) Disable client certificate forwarding to untrusted services

If you must call multiple services, instantiate separate sessions with specific certificates instead of a single client context that could be inadvertently used everywhere:

import requests

def get_partner_session():
    session = requests.Session()
    session.cert = ('partner_only.crt', 'partner_only.key')
    session.verify = '/etc/ssl/certs/partner-ca.crt'
    return session

session = get_partner_session()
resp = session.get('https://api.partner.com/metadata')

4) Enforce network-level controls and certificate pinning

Use Kubernetes NetworkPolicy or firewall rules to restrict egress to known endpoints. For higher assurance, pin the server certificate or public key hash in your verification logic to reduce reliance on CA trust alone:

import requests
import ssl
from django.http import JsonResponse
from django.views.decorators.http import require_GET

@require_GET
def fetch_with_pinning(request):
    url = request.GET.get('url')
    parsed = urlparse(url)
    if parsed.hostname != 'api.partner.com':
        return JsonResponse({'error': 'host not allowed'}, status=400)
    # Pin a specific public key hash (SPKI pin)
    session = requests.Session()
    session.cert = ('outbound.crt', 'outbound.key')
    session.verify = '/etc/ssl/certs/ca.pem'
    # Custom verification can enforce pin checks here
    resp = session.get(url)
    return JsonResponse({'status': resp.status_code})

These practices ensure that mTLS strengthens outbound calls without turning the Django app into a conduit to internal mTLS-protected assets. Security checks like those performed by middleBrick can validate that client certificates are not over-privileged and that the runtime behavior aligns with intended network boundaries.

Related CWEs: ssrf

CWE IDNameSeverity
CWE-918Server-Side Request Forgery (SSRF) CRITICAL
CWE-441Unintended Proxy or Intermediary (Confused Deputy) HIGH

Frequently Asked Questions

Does mTLS prevent SSRF in Django?
No. mTLS secures mutual authentication but does not prevent a compromised Django app from being used as a proxy to internal mTLS-protected services if user input is not strictly validated.
How can middleBrick help with SSRF and mTLS configurations?
middleBrick scans API endpoints and correlates OpenAPI specs with runtime findings, highlighting paths where SSRF may leverage mTLS client certificates and guiding remediation such as host allowlists and certificate scoping.