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=true in 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') (not all) on the Serializer.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() + a FormRequest with explicit rules, or $request→only(['username', 'email']).

  • Express + Mongoose: a Joi / Zod / yup schema 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

© 2026 Xygeni. All rights reserved