HIGH open redirectdjangomutual tls

Open Redirect in Django with Mutual Tls

Open Redirect in Django with Mutual TLS — how this specific combination creates or exposes the vulnerability

An open redirect occurs when an application redirects a user to a URL without validating that the destination is trusted. In Django, this commonly arises when a redirect target is derived from request data such as a query parameter. When mutual TLS (mTLS) is enforced, the server validates client certificates, which can create a false sense of security. Developers may assume that because the client is authenticated via mTLS, the request is inherently safe and skip proper validation of redirect targets. This combination is risky: mTLS secures the channel and verifies identity, but it does not constrain application logic. An authenticated client can still supply a malicious next_url parameter, and if the view uses Django’s redirect() without verifying the host, the authenticated session can be abused to steer users to phishing sites.

Consider a Django view that supports single-logout (SLO) or post-login redirects and uses a query parameter like next. With mTLS, the request will present a valid client certificate, and request.user may be populated via mTLS mapping (e.g., using SSL_CLIENT_S_DN or a middleware that maps certificates to users). If the view does not validate the next URL’s host against a whitelist, an attacker who possesses a valid client certificate (e.g., from a compromised device or a misissued cert) can craft a URL such as https://api.example.com/sso/logout?next=https://evil.com and trigger a redirect to the malicious site while the browser still carries the session context implied by mTLS authentication. The vulnerability is not in mTLS itself but in the missing validation layer; mTLS ensures who is talking, not whether the requested action is safe.

Django’s built-in redirect behavior can inadvertently allow open redirects when using django.shortcuts.redirect with a relative or absolute URL derived from user input. Even when using HttpResponseRedirect, if the URL is constructed from request.GET or request.POST without host normalization and validation, the open redirect persists. In an mTLS-enabled deployment, hosts may incorrectly believe that channel-level authentication substitutes for application-level checks, leading to missing host whitelisting and allowlist logic. Attack patterns such as leveraging compromised certificates or stolen tokens pair with open redirect flaws to create convincing phishing paths that retain the appearance of a trusted origin due to the presence of a valid client certificate.

Mutual TLS-Specific Remediation in Django — concrete code fixes

To remediate open redirects in Django when mTLS is in use, always validate redirect targets independently of authentication mechanisms. Never rely on mTLS to enforce safe destinations. Use Django’s is_safe_url (Django < 4.0) or django.utils.http.is_safe_url (Django 4.0+) to check that the URL’s host matches an allowed set, and prefer using absolute path redirects or explicit destination views instead of raw user-supplied URLs.

Example 1: Safe redirect with explicit destination and mTLS identity mapping

import os
from django.http import HttpResponseRedirect, HttpRequest
from django.conf import settings

def get_mtls_user_identity(request: HttpRequest):
    """Extract user identity from mTLS client certificate headers."""
    cert_dn = request.META.get('SSL_CLIENT_S_DN', '')
    # Map certificate DN to a local user, e.g., via a mapping table
    # This is illustrative; implement your own mapping logic.
    return cert_dn

def sso_logout(request):
    next_url = request.GET.get('next')
    allowed_hosts = {settings.ALLOWED_REDIRECT_HOST}
    # Validate the host against an explicit allowlist
    from django.utils.http import is_safe_url
    if not is_safe_url(url=next_url, allowed_hosts=allowed_hosts):
        next_url = '/'
    # Use redirect with a safe, validated next_url
    return HttpResponseRedirect(next_url)

Example 2: Middleware to normalize hosts and enforce strict redirect allowlist

from django.utils.http import is_safe_url

REDIRECT_ALLOWED_HOSTS = ['api.example.com', 'app.example.com']

class SafeRedirectMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)
        return response

    def process_template_response(self, request, response):
        # If response is a redirect, ensure the Location header is safe
        if hasattr(response, 'url') and response.status_code in (301, 302, 303, 307, 308):
            if not is_safe_url(url=response.url, allowed_hosts=REDIRECT_ALLOWED_HOSTS):
                response.url = '/'
        return response

Example 3: View using explicit destination mapping instead of raw next

from django.shortcuts import redirect

def home(request):
    # Map known, safe destinations by name rather than raw URLs
    destination = request.GET.get('dest', 'dashboard')
    safe_map = {
        'dashboard': '/dashboard/',
        'profile': '/profile/',
        'logout': '/accounts/logout/',
    }
    path = safe_map.get(destination, '/')
    return redirect(path)

Example 4: Configure host validation for is_safe_url in Django settings

# settings.py
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Ensure is_safe_url considers your public host
ALLOWED_REDIRECT_HOSTS = ['api.example.com', 'static.example.com']

Example 5: Unit test for redirect safety

from django.test import TestCase, RequestFactory
from django.utils.http import is_safe_url

class RedirectSafetyTests(TestCase):
    def setUp(self):
        self.factory = RequestFactory()

    def test_is_safe_url_with_allowed_host(self):
        url = 'https://api.example.com/settings'
        self.assertTrue(is_safe_url(url, allowed_hosts={'api.example.com'}))

    def test_is_safe_url_with_disallowed_host(self):
        url = 'https://evil.com/steal'
        self.assertFalse(is_safe_url(url, allowed_hosts={'api.example.com'}))

Frequently Asked Questions

Does mutual TLS prevent open redirect vulnerabilities?
No. Mutual TLS authenticates the client and protects the channel, but it does not validate redirect targets. You must still validate and restrict redirect URLs to trusted hosts.
What is a safe pattern for handling next/redirect parameters in Django with mTLS?
Avoid using raw user-supplied URLs for redirects. Use an allowlist of destinations or map logical names to paths, and validate with django.utils.http.is_safe_url against a defined ALLOWED_REDIRECT_HOSTS set.