VNX-PY-016: Django mass assignment via request data unpacking
Detect Python web code that writes the whole request body to a model: **request.data into create/update, SQLAlchemy .values() or a model constructor, Query.update(request body), __dict__.update(request.json), an unguarded setattr loop over request keys, or a ModelSerializer/ModelForm with fields = '__all__'.
| Field | Value |
|---|---|
| Rule ID | VNX-PY-016 |
| Severity | High |
| SARIF level | error |
| Kind | sast |
| Languages | Python |
| CWE | CWE-915 |
| CAPEC | CAPEC-78 |
| ATT&CK | T1565 |
| CWSS vector | CWSS:1.0/TI:H/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:H/DI:H/EX:H/EC:N/P:H |
| Tags | mass-assignment django python |
| Source | internal/sast/rules/vnx-py-016.rego |
Overview
This rule detects Django code that creates model instances by directly unpacking request data (e.g., Model.objects.create(**request.data)) or Django REST Framework serializers that expose all model fields via fields = '__all__'. Mass assignment occurs when an attacker includes unexpected fields in a request, such as is_staff, is_superuser, or price, and the application blindly persists them.
Why This Matters
Mass assignment is one of the most exploited web application vulnerabilities because:
- It requires zero special tools, an attacker simply adds extra fields to a normal HTTP request
- It can escalate privileges instantly (
is_admin: true,role: "superuser") - It can manipulate business logic (
price: 0,discount: 100,verified: true) - It bypasses form-level validation because the attacker sends raw JSON/POST data
- Django and DRF will silently accept and persist extra fields unless explicitly restricted
The OWASP API Security Top 10 lists mass assignment as API6:2023 – Unrestricted Access to Sensitive Business Flows.
What Gets Flagged
Pattern 1: Direct request data unpacking into model creation
# Flagged: all request data flows into model fields
User.objects.create(**request.data)
Profile.objects.create(**request.POST)
Pattern 2: DRF serializer with fields=‘__all__’
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
# Flagged: exposes every field including is_staff, is_superuser, password
fields = '__all__'
Pattern 3: The body copied onto an object, or written key by key
# Flagged
user.__dict__.update(request.get_json())
for key, value in request.data.items():
setattr(customer, key, value) # flagged: no allowlist test in the loop
payload = request.json
db.session.add(Order(**payload)) # flagged: model constructor in an ORM file
Not flagged: explicit keyword arguments, a dict built from an allowlist, serializer.validated_data or form.cleaned_data, pydantic or dataclass constructors, a setattr loop guarded by if key in ALLOWED, and the pattern inside a comment or string.
The rule applies only to .py files.
Remediation
-
Explicitly list the fields you intend to set. Never unpack all request data into a model:
# Safe: only the expected fields are used User.objects.create( username=request.data["username"], email=request.data["email"], ) -
Use Django Forms or DRF Serializers with explicit field lists. These act as an allowlist for incoming data:
class UserSerializer(serializers.ModelSerializer): class Meta: model = User # Safe: only these three fields are accepted from input fields = ["username", "email", "bio"] # Extra safety: mark sensitive fields as read-only read_only_fields = ["is_staff", "is_superuser", "date_joined"] -
Use
read_only_fieldsfor sensitive attributes. Even with an explicit field list, mark fields that should never be set by users:class ProductSerializer(serializers.ModelSerializer): class Meta: model = Product fields = ["name", "description", "price", "category"] read_only_fields = ["price"] # Only admins can change price -
Validate and clean incoming data before model operations. Use Django’s form validation:
form = UserCreationForm(request.POST) if form.is_valid(): user = form.save() # Only form-declared fields are saved -
Use
excludecautiously, prefer explicitfields. Whileexcludeworks, it’s fragile: adding a new model field automatically exposes it unless you remember to update the exclusion list.
References
- CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes
- OWASP API Security Top 10 – API6:2023 Mass Assignment
- OWASP Mass Assignment Cheat Sheet
- CAPEC-78: Using Slashes in Alternate Encoding
- MITRE ATT&CK T1565 – Data Manipulation
- Django REST Framework Serializers Documentation
- Django ModelForm Documentation
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-py-016
// nosec vnx-py-016
Or record an ignore rule: vulnetix ignore add --rule vnx-py-016 --reason "...". See Suppressing findings.