| name | prevent-mass-assignment |
| description | Use when binding HTTP request parameters or JSON bodies directly to model objects, database records, or data transfer objects — any place where user-supplied fields are mapped to internal properties. |
| source | OWASP Mass Assignment Cheat Sheet (owasp.org/www-project-cheat-sheets); OWASP API Security Top 10 2023 API3; CWE-915 |
| tags | ["security","owasp","mass-assignment","parameter-binding","api","developer","input-validation"] |
Prevent Mass Assignment
Allowlist only the fields users are permitted to set when binding request data to model objects — never allow users to set internal fields like is_admin, role, account_balance, or created_at.
Why This Is Best Practice
Adopted by: OWASP API Security Top 10 2023 API3 (Broken Object Property Level Authorization) is entirely caused by mass assignment. Rails, Django, and Laravel have all had critical mass assignment CVEs: GitHub (2012, CVE-2012-2661, allowed pushing to any repo via mass assignment to ActiveRecord), Rails (CVE-2013-2615). Django REST Framework, FastAPI, Spring Boot, and ASP.NET Core all provide allowlist mechanisms. OWASP ranks this in the API Top 10 specifically because it's pervasive in modern auto-binding frameworks.
Impact: The GitHub 2012 mass assignment vulnerability (Egor Homakov) allowed any user to gain admin-level access by submitting user[admin]=1 in a form — exploited publicly to demonstrate the issue. Every framework that auto-binds request parameters to model attributes is vulnerable by default unless explicitly configured. One missing allowlist in a user update endpoint can allow privilege escalation.
Why best: Denylist approaches (listing which fields to block) require knowing all dangerous fields in advance — new fields added to the model become vulnerable automatically. Allowlist (permitting only explicitly safe fields) is safe by default: new fields are blocked until explicitly permitted.
Sources: OWASP Mass Assignment Cheat Sheet; GitHub CVE-2012-2661; OWASP API Security Top 10 2023; CWE-915
Steps
-
Define explicit allowlists per operation — different operations permit different fields:
from pydantic import BaseModel
from typing import Optional
class UserCreate(BaseModel):
username: str
email: str
password: str
class UserUpdate(BaseModel):
email: Optional[str] = None
display_name: Optional[str] = None
class UserResponse(BaseModel):
id: int
username: str
email: str
display_name: Optional[str]
@app.post('/users')
def create_user(data: UserCreate):
user = User(**data.dict())
db.save(user)
return UserResponse.from_orm(user)
-
Django — use fields on ModelForm and Serializer:
Rules
- The allowlist must be per-operation, not per-model — an admin endpoint may permit more fields than a user self-update endpoint.
- Nested objects need their own allowlists —
user[address][attributes][admin]=true bypasses flat parameter filters.
- Response serializers also need allowlists — don't accidentally return
password_hash, internal_token, or ssn in API responses.
- Prefer separate DTO/schema classes over
exclude lists — exclusion lists grow stale as models evolve.
Common Mistakes
- Using
exclude instead of fields — adding a new sensitive field to the model automatically exposes it. Always use fields (allowlist).
- One schema for all operations — a create schema and an update schema typically have different permitted fields; conflating them over-permits.
- Trusting field-level validation to catch privilege escalation — validation checks correctness of values, not permission to set them. They're different concerns.
- Forgetting nested / related model binding —
user.address = Address(**request.data['address']) can mass-assign the nested object too.