Mass Assignment in Django with Firestore
Mass Assignment in Django with Firestore — how this specific combination creates or exposes the vulnerability
Mass Assignment occurs when a Django application binds incoming HTTP request fields directly to a model or form without explicit allowlisting. When the backend uses Google Cloud Firestore as the persistence layer, the typical Django patterns for creating or updating documents can inadvertently write attacker-controlled fields into the database. Unlike relational databases, Firestore stores data as flexible documents, which means nested maps and dynamic fields can be populated unintentionally if the binding logic is too permissive.
Consider a Django view that accepts JSON to create a Firestore document. If the view deserializes request data with a broad dictionary unpacking (for example, DocumentModel(**request.data) or manually passing request.data into a document set operation), any field present in the request—such as is_admin, role, or permissions—can be written into the document. Because Firestore does not enforce schema-level blocklists, these unexpected fields are stored as long as the document update operation is allowed by security rules.
Django REST Framework (DRF) introduces another vector: writable nested representations and ManyRelatedField or JSONField can expose nested structures that map onto Firestore map fields. If field-level permissions or model-level save() overrides are not carefully designed, an attacker can escalate privileges by injecting keys that change access control decisions, such as setting is_superuser or modifying role assignments stored alongside the document.
Firestore security rules cannot fully compensate for application-level mass assignment because rules typically focus on read/write permissions per path rather than field-level allowlists. A rule that permits writes to a document may still allow an attacker to add or modify sensitive subfields if the backend does not validate the payload before the write. This shifts the responsibility to the application to explicitly whitelist fields before any Firestore mutation.
An example attack chain: an unauthenticated or low-privilege user sends a PATCH request with {"role": "admin", "bypass_mfa": true}. If the Django view merges this dictionary into the document reference without filtering, the Firestore document is updated with elevated permissions. Subsequent authentication checks that rely on Firestore-stored roles may incorrectly grant privileged access, leading to BOLA/IDOR or privilege escalation within the API’s authorization model.
Firestore-Specific Remediation in Django — concrete code fixes
Remediation centers on strict field allowlisting before any Firestore write and avoiding automatic mapping of request data onto model or document structures.
Use explicit field selection with Django models
Instead of passing request.data directly, define a list of permitted fields and construct the instance manually. This ensures only expected attributes reach Firestore.
ALLOWED_FIELDS = {"display_name", "email", "department"}
def create_user_profile(request):
data = request.data
filtered = {k: v for k, v in data.items() if k in ALLOWED_FIELDS}
user = UserProfile(**filtered)
user.save() # Firestore backend will persist only allowed fields
return Response(UserProfileSerializer(user).data)
Django REST Framework serializers with explicit fields
Define a serializer that lists every writable field. Avoid extra_kwargs = {'password': {'write_only': True}} alone; ensure read-only fields are not writable. For nested Firestore map fields, use separate serializers and validate structure explicitly.
from rest_framework import serializers
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = ("uuid", "display_name", "email", "department")
read_only_fields = ("uuid", "email")
def update(self, instance, validated_data):
# validated_data contains only allowed fields
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save() # Firestore write with only validated_data
return instance
Firestore document updates with explicit field paths
When writing directly to Firestore (e.g., via the Firebase Admin SDK), construct update dictionaries programmatically instead of forwarding raw request payloads. Use dot notation for nested fields only when necessary and validated.
from google.cloud import firestore
db = firestore.Client()
def update_user_role(user_id, role):
doc_ref = db.collection("users").document(user_id)
# Explicitly allow only the role field; no dynamic key expansion
doc_ref.update({
"role": role,
"updated_at": firestore.SERVER_TIMESTAMP
})
For bulk operations, validate each document’s intended changes against a schema or allowlist before calling set with merge options.
def safe_batch_update(updates):
# updates is a list of (doc_ref, allowed_updates_dict)
for doc_ref, data in updates:
allowed = {k: v for k, v in data.items() if k in ALLOWED_FIELDS}
if allowed:
doc_ref.set(allowed, merge=True)
Middleware or service-layer validation
Implement a validation layer that inspects payloads against expected schemas (for example using Pydantic or manual checks) before any Firestore interaction. This catches unexpected keys early and prevents accidental writes of sensitive or administrative fields.
def validate_payload(payload, schema):
for key in payload.keys():
if key not in schema:
raise ValueError(f"Unexpected field: {key}")
# proceed with Firestore write
Related CWEs: propertyAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-915 | Mass Assignment | HIGH |