📋 What Is It?
Mass Assignment occurs when an API binds client-supplied input directly to the properties of an internal object - a database model, a domain entity, a configuration record - without an allow-list of the fields a client may actually set. The framework convenience that makes modern code pleasant (map the whole body onto an object in one line) becomes a vulnerability the moment that object holds server-controlled fields.
The attack is disarmingly simple: look at a legitimate request, guess the names of extra properties, and add them to the body. If binding is blind, the attacker has just written a field the UI never exposed - role=admin, isVerified=true, an inflated balance, or a re-pointed user_id.
⚠️ Common Exploits
- Privilege Fields: appending
"role":"admin"or"isAdmin":true - Verification Bypass:
"isVerified":true,"emailConfirmed":true - Financial Fields:
"balance","discount","credits","price" - Ownership Re-pointing:
"user_id"/"tenant_id"set to someone else - Nested Objects: deep binding through related models
- Timestamps & State:
createdAt,status,approvedBy - Prototype Pollution:
__proto__/constructorkeys in JS merges
🔴 Attack Flow
↓
2. Learn the object's real field names - GET responses, docs, spec, errors
↓
3. Append the sensitive fields to the request body
↓
4. Re-read the object to see which extras were persisted
↓
5. IMPACT: Self-granted privileges, altered balances, hijacked ownership!
❌ Vulnerable Code
✅ Secure Code
✓ Prevention Checklist
- Bind an explicit allow-list of writable fields - never the whole body
- Prefer schemas that reject unknown keys (
extra=forbid,additionalProperties: false) - Use dedicated request DTOs instead of database models
- Set identity, ownership, role, status, and timestamps on the server
- Define writable sets per role and per operation (create vs update)
- Avoid
Object.assign/deep-merge on untrusted input; block__proto__ - Never trust a deny-list - new columns default to exposed
- Validate nested objects with the same rigour as the top level
- Log and alert when unexpected fields arrive
- Test by appending sensitive fields and asserting they are not persisted
🔍 Detection & Tools
Validation:
Testing:
How to Test:
- Add
role,isAdmin,balance,user_idto create/update bodies - Re-read the object - did anything extra stick?
- Harvest field names from GET responses, errors, and the OpenAPI spec
- Try nested paths (
"profile": {"tier": "gold"}) - Send
__proto__/constructorkeys against JS merge code
🌍 Real-World
These are documented incident classes - recurring patterns rather than single CVEs.
- Self-granted administrator: a registration or profile-update endpoint binds a
rolefield the UI never sends, and the account comes back as admin. - Verification bypass: setting
isVerified/emailConfirmedin the body skips the whole confirmation flow. - Ownership hijack: passing another user's identifier on create re-points a record - invoice, ticket, or device - at the victim.
- Commerce manipulation: price, discount, or loyalty-point fields accepted from the client at checkout.
- Prototype pollution: deep-merge helpers on untrusted JSON corrupt object prototypes and change downstream behaviour.
📌 Quick Tips
- DO NOT pass the request body into a model constructor
- DO NOT rely on a deny-list of "dangerous" fields
- DO allow-list writable fields per role and operation
- DO set ownership and privilege server-side
- DO reject unknown keys with a 4xx
📜 Compliance
Related Standards:
- OWASP API Security Top 10 2019 - API6
- Merged into API3:2023 Broken Object Property Level Authorization
- CWE-915 Improperly Controlled Modification of Dynamically-Determined Object Attributes
- CWE-1321 Prototype Pollution
- CWE-639 Authorization Bypass Through User-Controlled Key