Mass assignment
CWE-915OWASP A01:2021Updated August 31, 20267 min read
Mass assignment is a vulnerability that lets an attacker overwrite fields that were never meant for them, because the application binds an entire request body onto a model. One extra field such as isAdmin or balance in the JSON is enough to become an administrator. The fix is an explicit allowlist of writable fields, ideally expressed as a dedicated DTO.
Almost every application copies fields from a form or a JSON body into an object that then goes to the database. Frameworks make that temptingly short: a single line of code sets every field from the request onto the model at once. That is exactly where mass assignment appears, because from that moment on the sender of the request decides which fields come along, not you.
What is mass assignment?
Mass assignment is a vulnerability that lets an attacker overwrite fields of an object that were never meant for them, because the application binds an entire request body onto a model without filtering it. Other ecosystems call the same problem autobinding, object injection or over-posting; the OWASP API Security Top 10 files it under Broken Object Property Level Authorization.
An everyday comparison: you join a sports club and fill in a form with your name, address and date of birth. At the bottom of that form sit two boxes that normally only the membership office completes, namely “fees paid” and “board member”. If the clerk copies the completed form into the membership system without reading it, the applicant decides for themselves whether they have paid and whether they sit on the board. Mass assignment is exactly that unchecked transfer.
The heart of the problem is that a model almost always has more fields than the form shows. A user record holds not only a name and an email address but also fields such as isAdmin, role, emailVerified, organisationId, credits or passwordHash. The form displays three of them; the binding accepts all of them. An attacker does not have to bypass anything: they simply add a field to the JSON they were already sending.
That places mass assignment under Broken Access Control. Authentication is not what fails here, authorisation at field level is. The user is allowed to edit their own profile, which is correct. They are just not allowed to edit every field of that profile, and it is precisely that distinction which is missing.
How does a mass assignment attack work?
Take an endpoint where a signed-in user updates their own profile. The developer loads the user object, copies the request body onto it and saves it again.
Vulnerable:
app.patch("/api/users/me", requireLogin, async (req, res) => {
const user = await User.findById(req.session.userId);
// The entire request body is written onto the model
Object.assign(user, req.body);
await user.save();
res.json({ id: user.id, email: user.email });
});
At first glance the authorisation looks fine: requireLogin keeps anonymous visitors out and the user only ever edits their own record. Yet req.body decides in full which columns get written. The attacker opens their profile page, intercepts the request and adds two fields:
PATCH /api/users/me HTTP/1.1
Host: app.example.com
Content-Type: application/json
Cookie: session=b1c9f4a2
{"displayName":"Mo","isAdmin":true,"credits":100000}
They do not need to guess those field names. The names are usually right there in the response of the GET endpoint for the same object, in an OpenAPI schema, in a GraphQL introspection, or in the JavaScript bundle that ships the admin screen. After this single request the attacker is an administrator. There is no injection, no exploit and no unusual traffic pattern: it is a valid request to a valid endpoint with two extra fields.
The fix is to stop trusting the request body as a whole and instead pull out explicitly what a user may change about themselves. You do that with a DTO, in full a data transfer object: a separate type that describes exactly the permitted fields and rejects the rest.
Secure:
import { z } from "zod";
// DTO: exactly the fields a user may change about themselves
const ProfileUpdate = z
.object({
displayName: z.string().min(1).max(80),
locale: z.enum(["nl", "en", "de"])
})
.partial()
.strict(); // an unknown field raises an error instead of passing silently
app.patch("/api/users/me", requireLogin, async (req, res) => {
const parsed = ProfileUpdate.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: "Invalid field in request body" });
}
const user = await User.findById(req.session.userId);
// parsed.data contains allowlisted fields only
Object.assign(user, parsed.data);
await user.save();
res.json({ id: user.id, email: user.email });
});
Two things changed. parsed.data holds allowlisted fields only, so isAdmin never reaches the model no matter what the body contained. And thanks to strict, an unknown field is actively rejected rather than quietly dropped. That is the difference between an attacker who can keep probing undisturbed and a log line that tells you somebody is trying.
The same idea exists in every framework under its own name: strong parameters in Rails, the fillable property in Laravel, a dedicated DTO class in Spring, and an explicit bind list in ASP.NET Core.
organisationId, teamId or roleId move an account to another tenant or another role without isAdmin ever appearing in the body.What is the impact of mass assignment?
The impact depends entirely on which field is writable, which is why severity runs from medium to high. If only a cosmetic field can be overwritten, a timestamp or an internal note, it stays an integrity problem with no immediate consequence. If the model carries a privilege field, the result is full privilege escalation: the attacker makes themselves an administrator and inherits everything that role may do, in one request.
Between those two sits the category that hurts commercially. A writable balance, credits, price or discount turns a checkout flow into self-service, with the attacker setting their own balance or their own price. A writable organisationId or tenantId breaks the separation between customers in a multi-tenant environment, which in practice means access to another customer’s data. And a writable emailVerified, email or passwordResetToken opens a route to account takeover.
In business terms that adds up to manipulated revenue, a data breach with a notification duty, and an integrity problem that is hard to reconstruct afterwards. The last of those is underrated: because the request looks entirely normal, many log files hold no trace of when and how an account became an administrator.
How do you detect mass assignment?
Start with the read model. Fetch an object through the API and lay the fields in the response next to the fields that the matching write endpoint officially accepts. Every field you get back but are not supposed to send is a candidate. Further sources are an OpenAPI specification, a GraphQL introspection, hidden form inputs, the JavaScript bundle of the admin screen, and the model definitions in the source code.
Then you test by adding one extra field to an otherwise ordinary request and fetching the object again to see whether the value really persisted. Mind the difference between ignored and applied: a 200 response says nothing, the second read does. If the value does not visibly change in the response but the behaviour does, for instance because an admin menu suddenly appears, the effect is there all the same.
Automated scanners perform poorly here, because they do not know what a field name means and cannot judge the consequences of a successful binding. Finding it is manual work that relies on knowledge of the data model and of the roles in the application. AssistSec covers mass assignment in penetration tests of APIs and web applications, and shows per finding which field was written and what an attacker gained by it.
How do you prevent mass assignment?
- Never bind straight onto your database model. Let a request land in a DTO that knows only the fields of that one endpoint, then copy field by field onto the model.
- Use an allowlist, not a blocklist. A list of forbidden fields forgets the field somebody adds to the model next month; a list of permitted fields does not.
- Reject unknown fields explicitly. Answer with a 400 and record the attempt. Dropping them silently is safer than accepting them, but it gives you no signal at all.
- Keep privilege fields out of every user-facing write path. Roles, balances, prices and verification flags are set server-side, through a separate action with its own authorisation check.
- Make the allowlist role-aware. What an administrator may write is a different set from what the owner of a record may write; use two schemas rather than one schema with an exception in it.
- Turn on your framework’s protection and keep it current. Strong parameters in Rails,
fillablein Laravel and explicit bind lists in Spring and ASP.NET Core only work if every new model is added to them. - Log changes to sensitive fields. An audit entry for every change of a role, a balance or a verification flag makes abuse visible after the fact and is often the only trace that remains.
Sources
- CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributescwe.mitre.org
- OWASP Cheat Sheet: Mass Assignmentcheatsheetseries.owasp.org
- OWASP API Security Top 10: Broken Object Property Level Authorizationowasp.org
- Ruby on Rails Guides: Action Controller Overview (strong parameters)guides.rubyonrails.org
Frequently asked questions
What is the difference between mass assignment and IDOR?
With IDOR an attacker reaches an entire object that is not theirs, usually by changing an id in the URL. With mass assignment they stay on their own object but write a field they are not allowed to change. IDOR is an authorisation failure at object level, mass assignment is an authorisation failure at field level. Both sit under Broken Access Control and are tested side by side.
Do modern frameworks protect against mass assignment by default?
Partly, and only when you use the protection. Rails has strong parameters, Laravel has the fillable property, Spring and ASP.NET Core support explicit bind lists and separate DTO classes. Those mechanisms are opt-in per model or per endpoint, so a new model or a quickly added endpoint falls outside them by default. The protection is only as current as the last developer who remembered it.
Is it enough to leave a sensitive field out of the API documentation?
No. An undocumented field is still bound as soon as its name is known, and that name is usually visible in the response of the read endpoint, in a GraphQL introspection, or in the JavaScript bundle of the admin screen. Removing it from the documentation changes nothing about the behaviour of the server. Only a server-side allowlist is a real control.
How do I test whether an endpoint is vulnerable to mass assignment?
Fetch the object with a GET first and note every field in the response. Then add one sensitive-looking field to an otherwise normal write request, for example isAdmin or role, and fetch the object again. A 200 response proves nothing; only the second read shows whether the value was actually stored. Watch for indirect effects too, such as an admin menu that appears after the request.
Related articles
- VulnerabilitiesCWE-284A01:2021Broken access controlBroken access control explained: horizontal and vertical privilege escalation, forced browsing, and how deny by default fixes it server-side.
- VulnerabilitiesCWE-639A01:2021Insecure direct object reference (IDOR)IDOR explained: how attackers tamper with IDs in URLs or APIs to read or change other users' data, and how to detect and prevent this vulnerability.
- VulnerabilitiesCWE-269A01:2021Privilege escalationPrivilege escalation explained: how attackers gain admin rights through a client-trusted role field or an unprotected admin route, and how to stop it.
- VulnerabilitiesCWE-1321A03:2021Prototype pollutionPrototype pollution explained: how __proto__ slips through a recursive merge into Object.prototype, what an attacker gains, and how to close the route.