Bola Idor in Django with Mutual Tls
Bola Idor in Django with Mutual Tls — how this specific combination creates or exposes the vulnerability
Broken Object Level Authorization (BOLA) is an API security risk where an attacker can access or modify objects they should not be allowed to. In Django, this commonly manifests when object-level permissions are checked only at the view entry point or rely solely on user-level ownership fields without validating per-request permissions. When you add Mutual Transport Layer Security (Mutual TLS) to Django, the server now requests and validates a client certificate before establishing an HTTPS connection. The intent is stronger identity assurance: the client presents a certificate that maps to an identity (for example, a username or API consumer ID) that the server can trust.
However, Mutual TLS alone does not enforce object-level permissions. A typical configuration terminates TLS at a reverse proxy (such as Nginx or a load balancer), extracts the client certificate subject (e.g., CN=alice), and forwards that identity to Django via a trusted header (e.g., SSL_CLIENT_S_DN_CN or a custom header). If Django then uses that identity only to filter querysets at a high level (e.g., MyModel.objects.filter(owner=request.user)), but does not revalidate ownership for each specific object ID in parameters, BOLA remains possible. An attacker who knows or guesses another user’s object ID can send a request with their own valid certificate (so TLS succeeds), yet the backend may return or act on an object owned by a different identity because the per-request check is missing or inconsistent.
Consider an endpoint like GET /api/invoices/{invoice_id}/ that maps invoice_id to an Invoice record with an owner field. If the view does not enforce Invoice.objects.filter(owner=request.user, pk=invoice_id).exists() before returning data, a user with a valid certificate for Alice can request invoice_id=42 that belongs to Bob, and the server may incorrectly serve Bob’s invoice. The same issue arises in POST/PUT/DELETE when the URL or body includes an object identifier but the backend trusts the URL parameter more than a strict per-action authorization check. Mutual TLS provides channel integrity and client identity, but it does not replace object-level authorization; without it, BOLA persists despite authenticated TLS sessions.
Another subtle interaction is that Mutual TLS can make developers assume stronger security than is implemented. For example, a proxy might be configured to require client certificates and map them to a user model, but the mapping may be incomplete (no certificate revocation checks) or the mapping may be cached in a way that does not reflect recent permission changes. If Django uses a cached user object without rechecking the request’s authorization scope for each operation, BOLA vulnerabilities remain exploitable. Additionally, if the endpoint exposes nested resources or indirect references (e.g., using a short-lived token derived from a certificate subject instead of the object’s true ownership), attackers can manipulate those indirect references to traverse object boundaries.
To summarize, BOLA in Django with Mutual TLS emerges when the developer conflates transport identity with object ownership. Mutual TLS ensures the client possesses a valid certificate and that the channel is encrypted, but it does not guarantee that the authenticated identity is allowed to access the specific object in question. The vulnerability is exposed when authorization checks are incomplete, overly permissive, or applied at the wrong layer, allowing a certificate-authenticated user to operate on objects that belong to other users.
Mutual Tls-Specific Remediation in Django — concrete code fixes
Remediation centers on enforcing object-level authorization in every relevant view and ensuring that the identity derived from the client certificate is correctly mapped to a Django user and used in all permission checks. Below are concrete patterns and code examples.
1. Map client certificate to a Django user reliably
Use a middleware that extracts the certificate subject and retrieves or creates a Django user. Keep the mapping explicit and avoid caching user identity in a way that bypasses permission checks.
import ssl
from django.conf import settings
from django.contrib.auth import get_user_model
from django.utils.deprecation import MiddlewareMixin
User = get_user_model()
class MutualTlsUserMiddleware(MiddlewareMixin):
def process_request(self, request):
# Example header set by the reverse proxy when client cert is validated
cert_subject = request.META.get('SSL_CLIENT_S_DN_CN')
if cert_subject:
# Ensure a user exists for this certificate subject
user, _ = User.objects.get_or_create(username=cert_subject, defaults={
'certificate_subject': cert_subject,
# other fields as needed
})
request.user = user
else:
request.user = None
2. Enforce object-level ownership in class-based views
Override get_queryset to scope objects to the request user, and use get_object to re-validate ownership for each request.
from django.views.generic import DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404
class InvoiceDetailView(LoginRequiredMixin, DetailView):
model = Invoice
slug_field = 'pk'
slug_url_kwarg = 'invoice_id'
def get_queryset(self):
# Always scope to the authenticated user's invoices
return Invoice.objects.filter(owner=self.request.user)
def get_object(self, queryset=None):
# Re-validate that the requested object belongs to the user
if queryset is None:
queryset = self.get_queryset()
obj = get_object_or_404(queryset, pk=self.kwargs[self.slug_url_kwarg])
return obj
3. Enforce object-level ownership in function-based views
Use a decorator or explicit checks to ensure the object belongs to the requesting user before performing any action.
from django.shortcuts import get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
@login_required
def update_invoice(request, invoice_id):
invoice = get_object_or_404(Invoice, pk=invoice_id, owner=request.user)
# Proceed with update logic
# ...
return redirect('invoice_detail', invoice_id=invoice.id)
4. Use Django’s built-in permission mixins for APIs
If using Django REST Framework, combine IsAuthenticated with object-level permissions.
from rest_framework.permissions import BasePermission
class IsOwnerOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# but write permissions require ownership
if request.method in ('GET', 'HEAD', 'OPTIONS'):
return True
return obj.owner == request.user
# In your viewset
from rest_framework import viewsets
class InvoiceViewSet(viewsets.ModelViewSet):
queryset = Invoice.objects.all()
serializer_class = InvoiceSerializer
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
def get_queryset(self):
return Invoice.objects.filter(owner=self.request.user)
5. Avoid insecure shortcuts
- Do not rely only on the presence of a client certificate to authorize object access; always couple identity with object ownership checks.
- Do not cache user-to-certificate mappings in a way that prevents revalidation of permissions after role or ownership changes.
- Ensure your reverse proxy’s certificate extraction headers are trusted (e.g., restrict to internal proxy IPs) to prevent header spoofing.
By combining Mutual TLS for transport identity and strict, per-request object-level authorization in Django, you ensure that a valid client certificate does not bypass ownership checks. This closes the gap where BOLA could be exploited even when TLS client authentication is in place.
Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |