Mass Assignment
ID |
mass_assignment |
Severity |
high (privilege fields) / low (identity / financial fields) |
Remediation Complexity |
medium |
Remediation Risk |
medium |
Remediation Effort |
medium |
Family |
API3:2023 - Broken Object Property Level Authorization |
CWE |
CWE-915 |
Resource |
data_exposure |
Language |
any |
Description
Walks each endpoint’s request body and parameter list for protected-attribute field names that the caller shouldn’t be allowed to set — typically privilege fields (admin, role, is_admin, is_staff, scopes, permissions) and identity / financial fields (owner, userId, balance, verified).
Severity is tiered:
-
HIGH — privilege fields (the GitHub-2012 / Rails mass-assignment class of vulnerability — write
is_admin=truein the signup form). -
LOW — identity / financial fields (advisory; some legitimate flows write these).
The field list comes from the request body’s resolved type where discovery provides one. Go payload types are read from struct json: tags; for Laravel the route is followed to its controller action and on to the Eloquent model’s $fillable, which is the set the framework itself permits mass assignment on. JS/TS handlers that never declare a typed body have their shape inferred from what they read — const { name, role } = req.body and req.body.role.
A model declaring $guarded = [] has no field list to walk at all, since every column is then assignable and the writable set lives in a migration rather than the model. That case is reported against the model instead, by mass_assignable_model.
Rationale
Modern web frameworks bind incoming JSON bodies directly to ORM entities (Rails attributes=, Spring @RequestBody MyEntity, Django Model(**request.data), ASP.NET model binding, Laravel fill($request→all())). The mechanism is ergonomic — and dangerous when the entity has fields the request was never supposed to set.
The canonical pattern is the signup endpoint that accepts {username, password, email} but is implemented by binding to a User entity that also has an isAdmin boolean. A caller who adds "isAdmin": true to the JSON gets an admin account at sign-up time — no further exploit needed. The 2012 GitHub Rails breach was exactly this pattern. It still re-appears in modern code regularly.
Identity and financial fields are the lower-tier variant: write userId and the new record is owned by someone else; write balance and the value is whatever the caller specified. These are LOW because legitimate flows sometimes write them (admin tooling, internal mutators) — but the same field being settable from an end-user endpoint is a finding.
Remediation
The fix is to never bind a request body directly to a persistence model. Use an explicit allowlist:
-
Spring: define a dedicated request DTO (record / class) with only the fields the operation accepts.
@RequestBody CreateUserRequest req, then map to the entity in the service layer. Do not annotate the entity itself with@RequestBody. -
Django / DRF: declare
fields = ('username', 'email')(notall) on theSerializer.Meta. -
FastAPI: use a Pydantic model whose fields are exactly the accepted inputs. Pydantic ignores unknown fields by default; set
model_config = ConfigDict(extra='forbid')to reject them outright. -
Rails:
params.require(:user).permit(:username, :email)— strong parameters. -
Laravel:
$request→validated()+ aFormRequestwith explicit rules, or$request→only(['username', 'email']). -
Express + Mongoose: a Joi / Zod /
yupschema in front of the route;Model.create(validated).
If the ORM auto-binds the full body and you cannot stop that, strip the protected fields server-side before persisting (req.body.isAdmin = undefined), and add a database-level constraint (CHECK (is_admin = false) for the user-signup path) as a safety net.
Configuration
The detector ships with a built-in list of privilege fields (HIGH-tier) and identity / financial fields (LOW-tier). Both lists are extensible via the detector configuration — add project-specific protected attributes if your domain has them (isSuperuser, creditLimit, tenantId, …).
Owner references. Among the protected fields a finding lists, the ones that name the object’s owner (owner_id, UserId, account_id, created_by, …) are repeated under owner_reference_fields. On a create they are the fields that let a caller file an object under somebody else’s name, an object-level question rather than a privilege one. The property only ever selects from fields already classified as protected, so it follows your configured lists.
Unless your project has bespoke privilege fields, you typically do not need to configure this detector.
References
-
OWASP API Security Top 10 (2023) - API3:2023 - Broken Object Property Level Authorization.
-
CWE-915 : Improperly Controlled Modification of Dynamically-Determined Object Attributes.
-
OWASP Cheat Sheets Series: Mass Assignment Cheat Sheet.
-
OWASP API3:2023 walk-through (mass assignment is the write side of API3; excessive data exposure is the read side).