Mass Assignment Lets Users Set Fields You Never Exposed

Key takeaway: Convenience binding assigns every matching field from untrusted input. The attacker does not need a form containing the field — they only need to know its name.
How the Flaw Appears
A profile update endpoint accepts a JSON body and passes it to the ORM to update the user record. It works, the code is two lines, and the form only submits name and email.
The attacker sends the same request with additional keys: "role": "admin", "email_verified": true, "account_balance": 100000, "tenant_id": 7. Any of those matching a column on the model gets written.
No vulnerability was introduced by a mistake in logic. The framework did exactly what it was asked — bind the input to the object. The mistake was treating the request body as a description of intent rather than as attacker-controlled data.
Why It Survives Review
The dangerous code reads as clean and idiomatic. A single call updating a record from a request body is what framework documentation shows, and it appears in every tutorial.
Field names are also easier to discover than teams assume. They appear in API responses, in GraphQL introspection, in error messages, in client bundles, and in the public documentation of whatever framework you use. Guessing is_admin requires no research.
The exposure grows silently too. A field added to the model next quarter for internal state becomes writable through an endpoint written a year earlier, with nobody involved in either change realising the connection.
The Reliable Fix
| Approach | Effectiveness |
|---|---|
| Bind request body directly to model | Vulnerable by default |
| Blocklist sensitive fields | Fails when a field is added |
| Explicit allowlist of permitted fields | Safe |
| Separate input type per endpoint | Safe and self-documenting |
The distinction between blocklist and allowlist is the same as in input validation, and it fails for the same reason. A blocklist protects the fields you remembered; a new column added later is writable until someone updates the list, and nothing prompts them to.
Defining a dedicated input type for each endpoint is the strongest pattern. The type contains exactly the fields a client may set, validation applies to it, and a model change cannot silently widen what is writable because the input type is a separate declaration.
This also documents the API. Reading the input type answers what the endpoint accepts, which is otherwise a question requiring inspection of the model and the handler together.
The Authorisation Dimension
Some fields are legitimately writable by some callers and not others. An administrator may set a role; a user may not.
Handling that with one endpoint and conditional field filtering is error-prone, because the condition has to be correct on every path. Separate endpoints with separate input types are clearer: a user profile endpoint that cannot express a role change at all, and an administrative endpoint with its own authorisation check.
Tenant identifiers deserve particular care. A writable tenant field in a multi-tenant system permits moving records between customers, which is among the most severe outcomes available. Those fields should never come from a request body — they belong to the authenticated session.
The Bottom Line
Never bind a request body directly to a persistence model. Define an explicit input type per endpoint containing only client-settable fields, take tenant and identity values from the session rather than the payload, and separate administrative operations into their own endpoints with their own types.



