Broken Access Control in Django with Mutual Tls
Broken Access Control in Django with Mutual Tls — how this specific combination creates or exposes the vulnerability
Broken Access Control occurs when API endpoints fail to enforce proper authorization checks, allowing one user to access or modify another user’s resources. In Django, this commonly maps to the OWASP API Top 10 A01: Broken Object Level Authorization (BOLA) / IDOR category. When Mutual TLS (mTLS) is used, developers may assume the client certificate alone is sufficient for access control. Relying exclusively on mTLS for authentication without implementing per-request authorization creates a gap: the identity derived from the client certificate is trusted, but authorization decisions are not enforced at the view or serializer level.
Mutual TLS verifies identity, not authorization. In Django, if views or viewsets do not explicitly check that the authenticated principal (e.g., via a certificate-derived user or attribute) is allowed to operate on the requested object, an authenticated client can iterate over IDs (e.g., /api/invoices/1/, /api/invoices/2/) and access or modify data that belongs to other tenants or roles. This becomes an IDOR when object-level permissions are missing. Additional risks appear when mTLS is used for entry-level authentication but the application introduces privilege escalation paths (e.g., role claims in client certificates) without validating those claims against a least-privilege policy, leading to BFLA / Privilege Escalation.
Consider an API that exposes sensitive invoice records. With mTLS, the request is considered authenticated, but if Django does not enforce row-level checks such as invoice.company_id == request.certificate_company_id, the unauthenticated attack surface includes vertical and horizontal privilege escalation. The scanner’s BOLA/IDOR and BFLA/Privilege Escalation checks will flag missing object-level authorization as a high-severity finding. Data Exposure and Encryption checks may also surface if sensitive fields are returned without proper field-level authorization, even when mTLS encrypts transit.
To contextualize, this aligns with real-world attack patterns such as CVE-2021-33517-type logic flaws where insufficient authorization allowed account takeover despite transport-layer security. middleBrick’s 12 checks run in parallel and will identify these gaps by correlating the unauthenticated (from the scanner’s perspective) endpoint behavior with spec-defined authentication schemes and runtime responses, producing a per-category breakdown with severity and remediation guidance.
Mutual Tls-Specific Remediation in Django — concrete code fixes
Remediation centers on two layers: continue using mTLS for strong client authentication, and enforce explicit, per-request authorization. Below are concrete patterns for Django and Django REST Framework that combine mTLS with robust access control.
1. mTLS setup in Django settings
Configure Django to require client certificates and map certificate fields to user identity without over-trusting them.
# settings.py
import ssl
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SSL_MIDDLEWARE_REQUIRE_CERT = 'optional_no_ca' # or 'require' if terminating at proxy
# If using django-sslserver for dev only:
# DEBUG = True
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'myapp.auth.MTLSCertificateAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
2. Custom mTLS authentication backend
Map certificate fields to a Django user and attach granular attributes for authorization use.
# myapp/auth.py
import ssl
from django.contrib.auth.models import User
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed, PermissionDenied
from rest_framework.request import Request
class MTLSCertificateAuthentication(BaseAuthentication):
def authenticate(self, request: Request):
cert = request.META.get('SSL_CLIENT_CERT')
if not cert:
return None
# Example: extract subject fields; adapt to your CA policy
subject = self._parse_subject(cert)
user_id = subject.get('email')
if not user_id:
raise AuthenticationFailed('Certificate missing user identifier')
try:
user = User.objects.get(email=user_id)
except User.DoesNotExist:
raise AuthenticationFailed('User not found for certificate')
# Attach certificate-derived attributes for later authorization checks
user.certificate_serial = subject.get('serial')
user.certificate_org = subject.get('organizationName')
user.certificate_cn = subject.get('commonName')
return (user, None)
def _parse_subject(self, cert_pem: str) -> dict:
# Simplified parsing; in production use cryptography.x509
# This is illustrative and should be replaced with robust parsing
import re
subject = {}
# Extract fields from the PEM subject
for line in cert_pem.strip().split('\n'):
if 'subject' in line.lower():
continue
# naive extraction for example
return subject
3. Enforce object-level authorization in views
Never rely on mTLS alone. Always check that the requesting principal is allowed to access the specific object.
# myapp/views.py
from rest_framework import viewsets, permissions
from .models import Invoice
from .serializers import InvoiceSerializer
class InvoiceViewSet(viewsets.ModelViewSet):
queryset = Invoice.objects.all()
serializer_class = InvoiceSerializer
def get_queryset(self):
qs = super().get_queryset()
# Enforce tenant/ownership based on certificate-derived attribute
if hasattr(self.request.user, 'certificate_org'):
return qs.filter(company_id=self.request.user.certificate_org)
return qs.none()
def perform_update(self, serializer):
obj = serializer.instance
if obj.company_id != self.request.user.certificate_org:
raise permissions.PermissionDenied('You cannot modify this invoice')
serializer.save()
4. Policy-based authorization with Django Guardian or custom checks
For fine-grained control, use object-level permissions and always verify on each request.
# myapp/authz.py
from rest_framework.exceptions import PermissionDenied
def user_can_access_invoice(user, invoice):
if not user.is_authenticated:
return False
return invoice.company_id == getattr(user, 'certificate_org', None)
# In a view or serializer
if not user_can_access_invoice(self.request.user, invoice):
raise PermissionDenied('Access denied to this resource')
5. Example OpenAPI spec with mTLS security scheme
Ensure your spec reflects mTLS as a security scheme so scanners correlate runtime behavior correctly.
# openapi.yaml (excerpt)
components:
securitySchemes:
mtls:
type: mutual_tls
description: Mutual TLS client certificate authentication
x-ssl-certificate: 'SSL_CLIENT_CERT'
security:
- mtls: []
paths:
/api/invoices/{id}:
get:
summary: Retrieve an invoice
security:
- mtls: []
responses:
'200':
description: OK
By combining mTLS authentication with explicit, per-request authorization checks, you retain strong identity assurance while preventing Broken Access Control and IDOR. middleBrick’s scans will validate that authentication is present and that authorization is enforced, reducing findings tied to BOLA/IDOR and BFLA/Privilege Escalation.