Nosql Injection in Django with Hmac Signatures
Nosql Injection in Django with Hmac Signatures — how this specific combination creates or exposes the vulnerability
NoSQL injection is a class of injection that targets databases such as MongoDB, CouchDB, and similar document stores where query syntax is expressed as JSON-like structures. In Django, developers sometimes build custom query logic that interpolates user input into dictionary structures before passing them to an ODM or a raw driver. When HMAC signatures are used only for integrity of selected parameters — for example, an API key, a user identifier, or a timestamp — but the signature does not cover the full query construction, an attacker can manipulate unchecked parameters to alter the resulting query.
Consider a Django view that accepts filter parameters as JSON and uses an HMAC to verify that the caller is allowed to request specific filters. If the application verifies the HMAC over a canonical string that excludes certain keys (such as a flexible filter object), an attacker can add new keys that the server incorporates into the query. Because the signature validates only a subset of the data, the server may trust the rest of the request and build a NoSQL query that reflects attacker-supplied values. This can lead to conditions equivalent to IDOR or BOLA when the injected query changes the set of returned documents, or worse, enables retrieval or modification of unauthorized records.
For example, an attacker might add a key like __raw__ or a nested operator used by the underlying database to change the semantics of the query. In MongoDB, operators such as $where, $ne, or $in can be embedded in the document sent to the server. If Django code constructs a dictionary from user data and then passes it to collection.find(filters), the injected operators become effective query operators. The HMAC, computed over a limited set of fields, does not detect the tampering, and the server executes the malicious query in the context of the permissions associated with the supplied authentication token or API key.
In practice, this pattern surfaces when developers try to offer flexible filtering while still wanting to authenticate and integrity-protect a subset of the request. The risk is especially pronounced when the HMAC covers only metadata such as user ID or a timestamp, while the filter payload is treated as trusted after signature validation. Because NoSQL injection exploits the structure of the query language rather than SQL syntax, traditional SQL-focused protections do not apply. The Django application must treat all user-influenced data that participates in query construction as untrusted, regardless of whether it is covered by an HMAC, and ensure that the HMAC scope or query building logic explicitly prevents injection through operators or unexpected keys.
Hmac Signatures-Specific Remediation in Django — concrete code fixes
Remediation focuses on ensuring that the HMAC scope covers all data that influences query construction and that query building never directly incorporates unchecked user input. Below are concrete, safe patterns for Django that combine HMAC verification with strict input handling.
Example 1: HMAC over the full filter payload
Compute the signature over the canonical JSON representation of the filters, ensuring that any addition or modification of keys invalidates the signature.
import json
import hmac
import hashlib
from django.http import JsonResponse
from django.views import View
SECRET_KEY = b'your-secret-key' # store in settings, use Django's SECRET_KEY or a dedicated key
def verify_hmac(data: dict, received_sig: str) -> bool:
canonical = json.dumps(data, sort_keys=True, separators=(',', ':'))
expected = hmac.new(SECRET_KEY, canonical.encode('utf-8'), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received_sig)
class FilteredListView(View):
def post(self, request):
payload = json.loads(request.body)
filters = payload.get('filters')
sig = payload.get('hmac')
if not verify_hmac(filters, sig):
return JsonResponse({'error': 'invalid signature'}, status=400)
# Build query using only known-safe fields; reject unexpected keys
allowed_keys = {'name', 'status', 'created_at'}
if not isinstance(filters, dict):
return JsonResponse({'error': 'invalid filters'}, status=400)
unexpected = set(filters.keys()) - allowed_keys
if unexpected:
return JsonResponse({'error': f'unexpected filter keys: {unexpected}'}, status=400)
# Safe: pass filters directly to the ODM/driver; no string interpolation
from myapp.models import MyDoc
qs = MyDoc.objects.filter(**filters)
results = list(qs.values('id', 'name', 'status'))
return JsonResponse({'results': results})
Example 2: Signed metadata plus strict schema validation
Use a schema validator for the query parameters and include only the metadata covered by the HMAC, keeping the signature small and focused while validating the rest independently.
from django.http import JsonResponse
from django.views import View
from pydantic import BaseModel, ValidationError
import hmac, json, hashlib
SECRET_KEY = b'secret'
class SignedRequest(BaseModel):
user_id: int
timestamp: int
filters: dict # validated separately
sig: str
def verify_hmac_scope(data: dict, sig: str) -> bool:
meta = {'user_id': data['user_id'], 'timestamp': data['timestamp']}
canonical = json.dumps(meta, sort_keys=True, separators=(',', ':'))
expected = hmac.new(SECRET_KEY, canonical.encode('utf-8'), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
class SafeQueryView(View):
def post(self, request):
try:
body = json.loads(request.body)
req = SignedRequest(**body)
except ValidationError as e:
return JsonResponse({'error': str(e)}, status=400)
if not verify_hmac_scope(body, req.sig):
return JsonResponse({'error': 'invalid signature'}, status=400)
# Strict schema for filters; reject unknown keys
allowed_filter_keys = {'name', 'status'}
if not isinstance(req.filters, dict) or set(req.filters.keys()) - allowed_filter_keys:
return JsonResponse({'error': 'invalid filters'}, status=400)
# Use parameterized lookups; avoid $where or raw expressions
from myapp.models import MyDoc
qs = MyDoc.objects.filter(user_id=req.user_id, **req.filters)
return JsonResponse({'count': qs.count()})
General guidelines
- Include all parameters that affect query construction in the HMAC scope, or validate them against a strict allowlist before they reach query-building code.
- Never construct query strings or dictionary keys by string interpolation with user input; use parameterized APIs provided by your ODM/driver.
- Treat NoSQL operators (e.g.,
$prefixed names) as reserved and reject them unless explicitly required and safely encoded. - Enforce schema validation on incoming JSON payloads to reject unexpected keys and malformed structures before they influence the query.