HIGH nosql injectiondjangohmac signatures

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.

Frequently Asked Questions

Why does HMAC over only a subset of fields still expose NoSQL injection in Django?
If the HMAC does not cover all inputs that influence query construction, an attacker can inject operators or unexpected keys into the unchecked portion of the request. The server may trust the signature and then build queries using attacker-controlled data, enabling NoSQL injection.
What is a safe alternative to flexible filter objects in Django to prevent NoSQL injection?
Use a strict allowlist of filter keys and validate inputs with a schema library (e.g., Django forms or Pydantic). Build queries using parameterized lookups rather than dynamically passing user dictionaries, and include all query-affecting data within the HMAC scope.