Bola Idor in Django with Hmac Signatures
Bola Idor in Django with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Broken Object Level Authorization (BOLA), also known as Insecure Direct Object References (IDOR), occurs when an API endpoint exposes a reference to an object (a resource ID, slug, or UUID) and relies solely on user identity for authorization, without verifying that the authenticated user owns or is permitted to access that specific object. In Django, this commonly manifests when a view uses a URL parameter such as /api/users/<int:user_id>/ or /api/records/<uuid:record_id>/ and retrieves the object via get_object_or_404 or Model.objects.get without confirming the requesting user’s relationship to that object.
Using HMAC signatures in Django can inadvertently create or expose BOLA when the signature is used to ensure integrity or authenticity of a parameter (e.g., an object ID) but does not enforce ownership or access control. For example, a developer may sign a user ID or record ID with HMAC to prevent tampering, then use the signed value in the URL. If the backend only validates the HMAC’s correctness and then directly uses the decoded object ID to fetch a record, the authorization check is still missing: the signature confirms the value hasn’t been altered, but it does not confirm that the requesting user is allowed to access the object identified by that ID.
Consider a Django endpoint that accepts a signed identifier for a document:
- The client includes a query parameter
doc_id_sig, which is an HMAC-signed version of the document’s primary key. - The server verifies the HMAC using a shared secret and extracts the
doc_id. - The server then does
Document.objects.get(pk=doc_id)and returns the document if it exists, without checking whether the requesting user has permission to view that document.
An attacker who knows or guesses another user’s document ID can craft a valid HMAC if the signing method is predictable or the secret is weak, or they may simply reuse a leaked signed token. Because the server trusts the signed parameter and skips ownership verification, the attacker can read, modify, or delete documents belonging to other users. This is a classic BOLA: the object-level authorization is bypassed by trusting a tamper-proof parameter instead of a proper access control check.
Django’s class-based views and generic views can make this easier to overlook. For instance, using SingleObjectMixin with get_object_or_404 based on a signed parameter that maps to a non-unique or non-user-scoped field can lead to IDOR. Even when using HMAC to bind a parameter to a specific user (e.g., signing the user’s primary key), failing to scope the queryset to the requesting user reintroduces the vulnerability.
Real-world API security checks, such as those performed by scanners like middleBrick, look for these patterns: unsigned versus signed parameter usage, missing ownership checks in the view or serializer, and endpoints where object-level permissions are implied by parameter validity rather than enforced by access control logic. In frameworks like Django, the combination of signed parameters and missing per-object authorization is a common root cause of BOLA.
Hmac Signatures-Specific Remediation in Django — concrete code fixes
To remediate BOLA when using HMAC signatures in Django, ensure that signature verification is coupled with explicit object-level authorization. The signature should bind a user or tenant identifier to the request, and every data access must scope the queryset to the requesting user or tenant, regardless of the signature’s validity.
Below are concrete, syntactically correct examples for Django views and serializers that combine HMAC verification with proper authorization.
1. HMAC verification utility
Use hmac and hashlib to sign and verify identifiers. Store the secret in settings and prefer django-environ or environment variables for production.
import hmac
import hashlib
import base64
from django.conf import settings
def verify_signed_object_id(signed_id: str, expected_user_id: int) -> bool:
"""
Verify that `signed_id` is a valid HMAC of '{expected_user_id}:{object_id}'
and that the embedded object_id matches an actual object the user may access.
Returns the object_id if valid, otherwise raises PermissionDenied.
"""
try:
decoded = base64.urlsafe_b64decode(signed_id.encode())
message, object_id_b = decoded.split(b':', 1)
object_id = int(object_id_b)
except (ValueError, TypeError, base64.binascii.Error):
raise PermissionDenied('Invalid signature format')
key = settings.HMAC_SECRET_KEY.encode()
mac = hmac.new(key, msg=message, digestmod=hashlib.sha256)
expected = base64.urlsafe_b64encode(mac.digest())
# Use constant-time comparison
if not hmac.compare_digest(expected, base64.urlsafe_b64encode(message + b':' + object_id_b)):
raise PermissionDenied('Invalid signature')
# At this point the signature is valid; still enforce ownership
return object_id
2. View using Signed ID with ownership check
In the view, after verifying the signature, scope the lookup to the requesting user.
from django.shortcuts import get_object_or_404
from django.http import JsonResponse
from django.core.exceptions import PermissionDenied
from .models import Document
from .utils import verify_signed_object_id
def document_detail(request, signed_doc_id: str):
user_id = request.user.id # or from JWT / session
object_id = verify_signed_object_id(signed_doc_id, user_id)
# Critical: scope to the requesting user to prevent BOLA
document = get_object_or_404(Document.objects.filter(pk=object_id, user=request.user), pk=object_id)
return JsonResponse({'id': document.id, 'title': document.title, 'content': document.content})
3. Django REST Framework serializer with ownership scoping
When using DRF, override get_queryset to ensure the queryset is always filtered by the requesting user, even when an HMAC parameter is used to select an object.
from rest_framework import viewsets, permissions, serializers
from django_filters.rest_framework import DjangoFilterBackend
from .models import Record
from .utils import verify_signed_object_id
class RecordSerializer(serializers.ModelSerializer):
class Meta:
model = Record
fields = ['id', 'name', 'owner']
def class RecordViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = RecordSerializer
# Always filter by owner to prevent BOLA
def get_queryset(self):
user = self.request.user
if user.is_anonymous:
return Record.objects.none()
return Record.objects.filter(owner=user)
def retrieve(self, request, signed_id=None):
if signed_id:
object_id = verify_signed_object_id(signed_id, request.user.id)
obj = get_object_or_404(self.get_queryset(), pk=object_id)
serializer = self.get_serializer(obj)
return Response(serializer.data)
return super().retrieve(request)
4. Signed URL pattern with user binding
When generating signed URLs, include the user ID and use a per-request nonce or timestamp to reduce replay risk. Verify both the signature and that the user in the URL matches the authenticated user.
import base64
import hmac
import hashlib
def make_signed_object_url(user_id: int, object_id: int, secret: bytes) -> str:
message = f'{user_id}:{object_id}'.encode()
mac = hmac.new(secret, msg=message, digestmod=hashlib.sha256)
sig = base64.urlsafe_b64encode(mac.digest()).rstrip(b'=').decode()
return f'/api/records/{object_id}?user_id={user_id}&sig={sig}'
# Example usage:
# signed = make_signed_object_url(user_id=42, object_id=123, secret=b'my-secret-key')
# Verify in view as shown above and ensure user_id matches request.user.id
5. Additional hardening recommendations
- Always scope queries to the requesting user or tenant; never rely on the signature alone for authorization.
- Use short-lived signed tokens or include a timestamp/nonce to limit replay windows.
- Employ Django’s permission classes and
get_querysetfiltering in views and DRF to enforce object-level permissions consistently. - Audit logs: record when a signed parameter is used and whether the subsequent object access is authorized.
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 |