Privilege Escalation in Django with Mutual Tls
Privilege Escalation in Django with Mutual TLS — how this specific combination creates or exposes the vulnerability
In Django, mutual Transport Layer Security (mTLS) means the server requests and validates a client certificate in addition to the server presenting its own certificate. When mTLS is configured but access controls are not explicitly tied to the contents of the client certificate, the authentication boundary can be misaligned with the authorization boundary, creating conditions for privilege escalation.
Django’s built-in authentication system primarily relies on username/password or token-based credentials. If you introduce mTLS at the reverse proxy or load balancer and then map the client certificate to a Django user (for example via a custom authentication backend), you must ensure that the mapping is strict and that authorization checks always use Django’s permission/authorization mechanisms rather than trusting the mere presence of a valid certificate.
A common misconfiguration is to allow mTLS to perform authentication and then assume the associated Django user has the minimal privileges needed. If role or group information is derived from the client certificate subject (e.g., from CN, O, or OU) but the server does not revalidate those claims against a trusted source on each request, an attacker who can influence the mapping or obtain a certificate for a low-privilege identity might escalate to a higher-privilege user. For example, if the proxy terminates mTLS and injects the certificate’s subject into headers (like SSL_CLIENT_S_DN) and Django uses those headers to assign groups without verifying the certificate chain and revocation status, a compromised or misissued certificate can lead to elevated permissions.
Another scenario involves unauthenticated LLM endpoints or unsafe consumption patterns where an API route mistakenly trusts mTLS alone and bypasses Django’s permission system. Even though the scan category LLM/AI Security includes unauthenticated LLM endpoint detection, the more immediate risk here is that mTLS authentication is treated as sufficient, leading to Insecure Direct Object References (IDOR) or BOLA when object-level permissions are not checked. The 12 security checks run in parallel by middleBrick include BOLA/IDOR and Authentication, which can surface these gaps when a valid client certificate grants access to objects that should be restricted by business logic.
To illustrate, consider an endpoint that displays user invoices. If the view only checks that a TLS client cert is valid and uses the certificate subject to look up a user, but does not ensure that the user can only access their own invoices, an attacker with a valid low-privilege certificate could manipulate the lookup (e.g., by changing the invoice ID in the URL) and access other users’ data. This is a BOLA/IDOR issue enabled by an authorization model that trusts mTLS authentication without enforcing object-level permissions.
middleBrick scans such endpoints in the unauthenticated attack surface and flags findings related to Authentication, BOLA/IDOR, and Privilege Escalation, providing severity and remediation guidance rather than fixing the logic for you.
Mutual TLS-Specific Remediation in Django — concrete code fixes
Remediation focuses on strict mapping between mTLS identities and Django authorizations, plus consistent use of Django’s permission system. Below are concrete patterns and code examples.
1. Custom authentication backend that validates mTLS and maps to Django user
Use a backend that extracts the certificate serial or subject, looks up the user, and ensures group/role membership is authoritative from Django’s database, not inferred solely from the certificate.
import ssl
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
User = get_user_model()
class MutualTLSBackend(ModelBackend):
def authenticate(self, request, client_cert=None):
if not client_cert:
return None
# Example: map certificate serial to user; adapt to your PKI
try:
user = User.objects.get(mtls_serial=client_cert.get('serial'))
except User.DoesNotExist:
return None
return user
def user_can_authenticate(self, user):
# Ensure user is active and allowed
return user.is_active and user.has_perm('api.can_access_mtls')
Configure in settings.py:
AUTHENTICATION_BACKENDS = [
'yourapp.backends.MutualTLSBackend',
'django.contrib.auth.backends.ModelBackend',
]
2. Enforce mTLS at the proxy and pass a verified header
If you terminate mTLS at a reverse proxy (e.g., Nginx), configure it to set a verified header only when the client certificate is valid, and make Django trust that header only when coming from the proxy.
# Nginx example (server context)
ssl_verify_client on;
ssl_client_certificate /path/to/ca.pem;
proxy_set_header SSL_CLIENT_CERT $ssl_client_escaped_cert;
In Django, read the header via a WSGI middleware that validates the proxy’s authenticity (e.g., checks a shared secret or a trusted IP) before using it:
class MutualTLSMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
cert_b64 = request.META.get('HTTP_SSL_CLIENT_CERT')
if cert_b64 and self._is_proxy_authorized(request):
# Decode and validate certificate, then map to user in request.user
request.user = self._map_cert_to_user(cert_b64)
else:
request.user = AnonymousUser()
response = self.get_response(request)
return response
def _is_proxy_authorized(self, request):
# Example: check a shared secret header or trusted IP
return request.META.get('HTTP_X_FORWARDED_PROTO') == 'https'
def _map_cert_to_user(self, cert_b64):
# Decode cert, extract serial, and fetch user
# Keep this logic aligned with MutualTLSBackend
from django.contrib.auth import get_user_model
User = get_user_model()
# Simplified; add cert parsing and error handling in production
serial = self._extract_serial_from_cert(cert_b64)
return User.objects.filter(mtls_serial=serial).first() or AnonymousUser()
def _extract_serial_from_cert(self, cert_b64):
# Use cryptography or pyOpenSSL to parse the cert; return serial as str
return 'extracted-serial' # placeholder
3. Authorization checks in views and APIs
Always use Django’s permission and ownership checks. Do not rely on the presence of a certificate to determine what a user can access.
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import get_object_or_404
from .models import Invoice
@login_required
@permission_required('api.view_invoice', raise_exception=True)
def invoice_detail(request, invoice_id):
invoice = get_object_or_404(Invoice, pk=invoice_id, user=request.user)
# Now safe: user is authenticated, has view permission, and owns the invoice
return render(request, 'invoice_detail.html', {'invoice': invoice})
For class-based views or Django REST Framework, use object-level permissions and ensure each lookup scopes to the requesting user.
from rest_framework import generics
from .models import Invoice
from .serializers import InvoiceSerializer
from rest_framework.permissions import IsAuthenticated
class InvoiceDetail(generics.RetrieveAPIView):
queryset = Invoice.objects.all()
serializer_class = InvoiceSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
# Always scope to the requesting user to prevent IDOR
return super().get_queryset().filter(user=self.request.user)
4. Revocation and certificate lifecycle
Ensure there is a process to revoke certificates and that your Django backend reacts to revocation. You can maintain a denylist of revoked serials or short-circuit authentication when a certificate is no longer trusted.
class MutualTLSBackend(ModelBackend):
def authenticate(self, request, client_cert=None):
if not client_cert or self.is_revoked(client_cert.get('serial')):
return None
# proceed as above
def is_revoked(self, serial):
# Check against a cache or database of revoked serials
return RevokedCertificate.objects.filter(serial=serial).exists()
These patterns align authentication via mTLS with robust authorization in Django, reducing the risk of privilege escalation while keeping the scan findings from middleBrick focused on actionable items rather than theoretical weaknesses.