Mass Assignment in Django with Hmac Signatures
Mass Assignment in Django with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Mass Assignment in Django occurs when a model is populated directly from user-supplied data, such as a request query or body, without explicitly restricting which fields can be set. This becomes especially risky when Hmac Signatures are used to bind a request to a particular operation but the application fails to validate or scope the signed payload against the model’s allowed fields.
Consider a Django model UserProfile with fields user, bio, and is_premium. If a view uses an Hmac Signature to verify that a request originated from a trusted source, but then passes the entire parsed payload to update(**data) or a model form, an attacker who can influence the signed data (e.g., by replaying or manipulating non-signature portions of the request) may set is_premium to True. The signature may validate correctly if the attacker knows or can forge the key, or if the signature covers only a subset of the payload while the rest is taken from user-controlled sources.
In a typical flow, the server generates an Hmac for a canonical representation of an action (e.g., user_id:action:resource_id) and includes it in a query parameter or header. The view then verifies the Hmac and proceeds to update the model using unchecked data from the request. Because mass assignment does not differentiate between safe and dangerous fields, sensitive attributes can be altered if they are included in the incoming data and not explicitly excluded.
This combination exposes two distinct attack surfaces: (1) the Hmac may not cover all fields that are bound to the model, and (2) even when the Hmac covers a token or identifier, the application may still trust other request parameters that were not integrity-protected. For example, an attacker could issue a legitimate, signed request to update a profile bio, then replay the signature while modifying the is_premium field in the POST body, relying on mass assignment to apply the unauthorized change.
Django’s form and serializer layers provide mechanisms to declare allowed fields, but if developers bypass these protections by manually constructing model instances or using QueryDict unpacking, the risk persists. The security issue is not the Hmac algorithm itself, but the lack of strict field allow-listing and the assumption that a verified signature implies safe data.
Hmac Signatures-Specific Remediation in Django — concrete code fixes
To mitigate mass assignment when using Hmac Signatures in Django, explicitly define which fields are permitted for each operation and apply them after successful signature verification. Never bind model updates directly to request.POST or request body without filtering by a declared allow-list.
Below are concrete, working examples that show how to combine Hmac verification with strict field handling.
Example 1: Hmac verification with an explicit field allow-list using dictionary comprehension.
import hmac
import hashlib
from django.http import JsonResponse
from django.views import View
class UpdateProfileView(View):
ALLOWED_FIELDS = {'bio', 'display_name', 'theme'}
SECRET_KEY = b'your-secret-key' # store securely, e.g., settings.SECRET_KEY
def post(self, request, user_id):
# Expecting: signature in header X-Signature, payload in request.body JSON
signature = request.META.get('HTTP_X_SIGNATURE')
payload = request.POST # or request.body parsed to dict
# Verify Hmac over canonical string; here we sign a sorted key=value concatenation
message = '&'.join(f'{k}={payload[k]}' for k in sorted(payload) if k != 'signature')
expected = hmac.new(self.SECRET_KEY, message.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
return JsonResponse({'error': 'invalid signature'}, status=403)
# Apply strict allow-list to prevent mass assignment
update_data = {k: payload[k] for k in payload if k in self.ALLOWED_FIELDS}
profile = UserProfile.objects.get(user_id=user_id)
for key, value in update_data.items():
setattr(profile, key, value)
profile.save()
return JsonResponse({'status': 'ok'})
Example 2: Using Django REST Framework serializers with Hmac verification on the viewset.
import hmac
import hashlib
from rest_framework import viewsets, status
from rest_framework.response import Response
from .models import UserSettings
from .serializers import UserSettingsSerializer
class HmacProtectedViewSet(viewsets.ModelViewSet):
queryset = UserSettings.objects.all()
serializer_class = UserSettingsSerializer
ALLOWED_FIELDS = {'theme', 'notifications_enabled'}
SECRET_KEY = b'your-secret-key'
def perform_create(self, serializer):
# Not used here, shown for completeness
pass
def perform_update(self, serializer):
# Verify Hmac before allowing update
signature = self.request.META.get('HTTP_X_SIGNATURE')
payload = self.request.data
message = '&'.join(f'{k}={payload[k]}' for k in sorted(payload) if k != 'signature')
expected = hmac.new(self.SECRET_KEY, message.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
raise serializers.ValidationError('invalid signature')
# Apply allow-list explicitly
filtered = {k: v for k, v in payload.items() if k in self.ALLOWED_FIELDS}
serializer.save(**filtered)
Example 3: Scoped Hmac that binds to specific action and resource, reducing replay risk.
import hmac
import hashlib
import time
from django.http import JsonResponse
SCOPE = 'profile:update'
def build_signature(user_id, action, timestamp, extra='', secret=b'secret'):
canonical = f'{SCOPE}:{user_id}:{action}:{timestamp}:{extra}'
return hmac.new(secret, canonical.encode(), hashlib.sha256).hexdigest()
class ScopedUpdateView:
def handle(self, request, user_id):
timestamp = request.GET.get('ts')
signature = request.GET.get('sig')
if abs(time.time() - int(timestamp)) > 300:
return JsonResponse({'error': 'stale request'}, status=400)
expected = build_signature(user_id, 'update', timestamp, extra='', secret=self.SECRET_KEY)
if not hmac.compare_digest(expected, signature):
return JsonResponse({'error': 'invalid signature'}, status=403)
# Use only explicitly allowed data, never raw request.data for model updates
profile = UserProfile.objects.get(user_id=user_id)
profile.bio = request.GET.get('bio', profile.bio) # explicit field mapping
profile.is_premium = False # never set from unsigned/user input
profile.save()
return JsonResponse({'status': 'ok'})Related CWEs: propertyAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-915 | Mass Assignment | HIGH |