Nosql Injection in Django with Cockroachdb
Nosql Injection in Django with Cockroachdb — how this specific combination creates or exposes the vulnerability
NoSQL injection occurs when untrusted input is interpreted as part of a NoSQL query rather than as data. While CockroachDB is SQL-compliant, Django’s integration and certain driver-level behaviors can expose NoSQL-like injection risks when constructing queries dynamically. In Django, using Q objects, F expressions, or raw QueryDict parsing without validation can allow an attacker to manipulate query structure through crafted parameters.
Consider a search endpoint that builds a filter from user-supplied JSON or URL query parameters. If the developer uses .filter(**kwargs) with unsanitized input, keys in the dictionary may map to field lookups, and values may be interpreted as query operators. For example, passing {"username__$regex": ".*", "$where": "true"} could trigger injection-like behavior in drivers that expose extended query syntax, especially when combined with ORM features that map to CockroachDB’s SQL layer in unexpected ways.
Django’s MongoDB-like query syntax (e.g., __in, __contains) can be abused if keys are not strictly controlled. An attacker might supply username__in with a string value, causing the ORM to generate queries that bypass intended filters. When using raw SQL through cursor.execute, improper concatenation of table or column names — even with CockroachDB — can lead to injection if identifiers are not properly quoted or whitelisted.
The risk increases when developers assume CockroachDB’s SQL compliance negates NoSQL injection concerns. Although CockroachDB does not natively support document-style operators, Django’s ORM can translate certain lookups into SQL that behaves like NoSQL traversal. For instance, using __has_key on a JSONField with unsanitized input may allow traversal into nested structures in ways that mirror document database injection. This is particularly dangerous if the application uses dynamic field lookups based on user input without restricting allowed fields.
Additionally, raw query building using string formatting in Django management commands or API views can directly inject SQL that CockroachDB executes. While this is not traditional NoSQL injection, the pattern mirrors NoSQL injection risks: untrusted data influencing query structure. Examples include constructing WHERE clauses via string concatenation or using extra with unescaped parameters. The absence of input validation or type coercion in these paths makes the endpoint vulnerable.
Cockroachdb-Specific Remediation in Django — concrete code fixes
Remediation focuses on strict input validation, using parameterized queries, and avoiding dynamic query construction. Always prefer Django’s ORM filtering with hardcoded field names and avoid passing user-controlled dictionaries directly to .filter() or .exclude().
Example 1: Safe filtering with whitelisted fields
from django.db.models import Q
from myapp.models import User
ALLOWED_SEARCH_FIELDS = {'username', 'email', 'created_at'}
def safe_search(params):
query = Q()
for key, value in params.items():
if key in ALLOWED_SEARCH_FIELDS:
query &= Q(**{f'{key}__icontains': value})
return User.objects.filter(query)
Example 2: Parameterized raw SQL with quoting
from django.db import connection
def get_user_by_id(user_id: int):
with connection.cursor() as cursor:
cursor.execute(
'SELECT * FROM myapp_user WHERE id = %s',
[user_id]
)
return cursor.fetchone()
Example 3: Safe JSONField key access
from django.db.models import F
# Only allow known top-level keys
user_data = User.objects.annotate(
config_value=F('config__data__approved_value')
).filter(config_value__gt=100)
Example 4: Using Django forms for validation
from django import forms
class UserFilterForm(forms.Form):
username = forms.CharField(max_length=100, required=False)
email = forms.EmailField(required=False)
def validated_filter(request):
form = UserFilterForm(request.GET)
if form.is_valid():
return User.objects.filter(
username__icontains=form.cleaned_data['username'],
email=form.cleaned_data['email']
)
return User.objects.none()
Example 5: Avoiding dynamic field lookups
# UNSAFE: dynamic lookup from user input
# User.objects.filter(**user_supplied_dict)
# SAFE: explicit mapping
FILTER_MAP = {
'status': 'status',
'min_score': 'score__gte',
}
def apply_filters(allowed_params):
filters = {}
for key, value in allowed_params.items():
if key in FILTER_MAP:
filters[FILTER_MAP[key]] = value
return User.objects.filter(**filters)