Mass Assignment in Django with Mutual Tls
Mass Assignment in Django with Mutual Tls — how this specific combination creates or exposes the vulnerability
Mass assignment in Django occurs when a view directly passes user-controlled data (for example, data parsed from a request body) to a model constructor or .save() without explicitly restricting which fields can be set. This commonly leads to BOLA/IDOR or privilege escalation when a malicious actor can set sensitive fields such as is_staff, is_superuser, or record ownership identifiers.
Mutual TLS (mTLS) changes how authentication is established but does not change how application-level authorization works. When mTLS is used, the client certificate is typically validated by the server or an API gateway, and the identity is mapped to a user or an attribute (e.g., subject DN or a mapped group) that is then attached to the request — often in headers like SSL_CLIENT_S_DN or via a custom middleware that sets request.user. This authentication step can create a false sense of security: developers may assume that because a client presented a valid certificate, any data submitted by that client is safe to trust.
The combination of mTLS and mass assignment becomes dangerous when an endpoint uses mTLS for authentication but then performs mass assignment on user input without field-level allowlisting. For example, an endpoint that maps a client certificate to a user and then does something like Profile.objects.create(**request.data) or user.update(**request.data) may allow a malicious user (if the certificate mapping is misconfigured or if the client is compromised) to overwrite sensitive fields such as role, is_active, or any foreign key references that affect authorization.
Consider an endpoint that maps mTLS to a user and updates profile data:
def update_profile_mtls(request):
# Assume request.user is set by mTLS middleware
profile = Profile.objects.get(user=request.user)
# Risky mass assignment: request.data may contain fields like 'role' or 'permissions'
for key, value in request.data.items():
setattr(profile, key, value)
profile.save()
return JsonResponse({'status': 'ok'})
If request.data includes a field such as role or is_superuser, and the view does not explicitly filter those keys, the authenticated user can escalate privileges. This is a classic mass assignment issue: the vulnerability exists in the application logic, not in the TLS layer. MiddleBrick detects such patterns by correlating authentication context (e.g., mTLS-derived user mapping) with unchecked input assignments across 12 security checks, including BOLA/IDOR and Privilege Escalation.
Another subtle risk appears when mTLS is used to authenticate a service-to-service call and the downstream service deserializes JSON into a model using mass assignment. Even with strong client authentication, an attacker who compromises a trusted service or manipulates an endpoint can inject unexpected fields if the receiving endpoint is not strict about which fields are permitted.
Mutual Tls-Specific Remediation in Django — concrete code fixes
To secure Django endpoints when using mutual TLS, treat the certificate-derived identity as the source of truth for authentication but continue to enforce strict authorization and input allowlisting for all user-controlled data. Below are concrete, actionable fixes and code examples.
1. Use a strict allowlist for writable fields
Never pass request.data directly to a model. Instead, define an explicit set of fields that are safe to update. This prevents attackers from injecting fields like is_staff or role.
SAFE_PROFILE_FIELDS = {'first_name', 'last_name', 'bio', 'avatar_url'}
def update_profile_mtls_safe(request):
# request.user is set by mTLS middleware
profile = Profile.objects.get(user=request.user)
data = {k: v for k, v in request.data.items() if k in SAFE_PROFILE_FIELDS}
for key, value in data.items():
setattr(profile, key, value)
profile.save()
return JsonResponse({'status': 'ok'})
2. Use Django forms or serializers with explicit fields
Django forms or DRF serializers provide a declarative way to specify which fields are allowed for creation or update. This is the recommended pattern when combined with mTLS authentication.
from django import forms
class ProfileUpdateForm(forms.ModelForm):
class Meta:
model = Profile
fields = {'first_name', 'last_name', 'bio'}
# Exclude sensitive fields explicitly
def update_profile_with_form(request):
form = ProfileUpdateForm(request.data, instance=request.user.profile)
if form.is_valid():
form.save()
return JsonResponse({'status': 'ok'})
return JsonResponse({'errors': form.errors}, status=400)
3. Map mTLS identity securely and avoid leaking certificate details into models
Map the client certificate to a user in middleware and ensure that sensitive certificate attributes are not written to user-modifiable models. For example, extract only the necessary mapping and store minimal metadata.
import ssl
def mtls_middleware(get_response):
def middleware(request):
cert = request.META.get('SSL_CLIENT_CERT')
if cert:
# Parse certificate safely; do not store raw cert on user profile
subject = parse_subject(cert)
user = User.objects.filter(email=subject.email).first()
if user:
request.user = user
else:
request.user = AnonymousUser()
return get_response(request)
return middleware
def parse_subject(cert_pem: str) -> SimpleNamespace:
# Simplified example: use cryptography library in production
from cryptography import x509
from cryptography.hazmat.backends import default_backend
cert = x509.load_pem_x509_certificate(cert_pem.encode(), default_backend())
email = cert.subject.get_attributes_for_oid(x509.NameOID.EMAIL_ADDRESS)
return SimpleNamespace(email=email[0].value if email else '')
4. Reject unexpected fields early
Add validation that rejects any fields not in the allowlist before processing. This can be done at the middleware or view level to fail fast.
EXPECTED_FIELDS = {'first_name', 'last_name', 'bio'}
def validate_no_extra_fields(data, allowed):
disallowed = set(data.keys()) - allowed
if disallowed:
raise ValueError(f'Unexpected fields: {disallowed}')
def update_profile_validated(request):
validate_no_extra_fields(request.data, EXPECTED_FIELDS)
profile = Profile.objects.get(user=request.user)
profile.first_name = request.data.get('first_name', profile.first_name)
profile.last_name = request.data.get('last_name', profile.last_name)
profile.bio = request.data.get('bio', profile.bio)
profile.save()
return JsonResponse({'status': 'ok'})
Related CWEs: propertyAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-915 | Mass Assignment | HIGH |