A profile-edit endpoint takes the submitted form and saves it. The code is one clean line, and that's exactly the problem:
app.patch('/api/me', async (req, res) => {
const user = await User.findById(req.session.userId);
Object.assign(user, req.body); // bind everything the client sent
await user.save();
res.json(user);
});The form on the page only has "name" and "email." But the User model also has role, isAdmin, and emailVerified. An attacker sends {"name":"Al","role":"admin","isAdmin":true} straight to the API — no form required — and Object.assign faithfully copies all three onto the user. They just promoted themselves. This is mass assignment (a.k.a. over-posting): the code trusted that the request body would contain only the fields the form displayed, and it never does.
Why it happens and what it costs
The vulnerability is born from a convenience feature. Every major framework offers a "bind the whole body onto the object" shortcut — Object.assign(model, body), Model.update(req.body), User(**request.data), user.update_attributes(params) — because writing field-by-field assignment is tedious. That shortcut binds whatever keys arrive, including ones no legitimate client would send.
The impact depends on which hidden fields exist on the model:
- Privilege escalation —
role,isAdmin,permissions,groupId - Trust/verification bypass —
emailVerified,kycStatus,approved - Financial tampering —
balance,credits,discountRate,priceOverride - Ownership hijacking —
userId,ownerId,tenantIdto reassign a record to yourself or read across tenants
None of these appear in the UI, which is exactly why they get missed — the developer reasons about the form, but the attacker talks to the API.
The fix: allowlist the fields a client may write
Never bind the whole body. Decide explicitly which fields each role is permitted to set, and copy only those. This is the same principle as parameterized queries and safe template rendering: separate what the client controls from what the server controls.
The principles
- Allowlist, never blocklist. List the fields a client may write and ignore everything else. A blocklist of "dangerous fields" always misses the one you add next quarter.
- Separate privileged writes. Fields that govern authorization or money get dedicated endpoints protected by an explicit authorization check — never editable through the general update path.
- Mark server-owned fields read-only at the model/serializer layer, so even a careless handler can't bind them.
- Return a narrow view. Respond with only the fields you meant to expose, so the response can't leak the existence of sensitive columns.
The one-liner: decide server-side exactly which fields the client may set, and bind only those — the request body is a suggestion, not an instruction.
Try it in the mass assignment simulation: add role: admin to a profile update and escalate, then switch to an allowlisted serializer and watch the extra field get dropped. It's a close cousin of IDOR / broken object-level authorization — both are the client reaching fields or records it shouldn't.
Frequently Asked Questions
Related posts
Race Conditions in Web Apps: When Two Requests Beat Your Check
Check-then-act logic that looks correct on paper breaks when two requests run at once. Here's how TOCTOU race conditions drain balances and reuse one-time codes, and how atomic operations and locks fix them.
Aug 11, 2026 · 7 min readInsecure Deserialization: How Loading Data Becomes Running Code
Deserializing untrusted data can execute code before your app reads a single field. Here's how gadget chains work in Python pickle, Java, PHP, and Node, and why JSON with a schema is the fix.
Aug 9, 2026 · 7 min readWebSocket Security: Why the Same-Origin Rules Don't Apply
WebSockets skip the protections you rely on for normal requests — CORS doesn't apply and cookies are sent automatically, enabling cross-site hijacking. Here's how to validate origins, authenticate connections, and secure real-time apps.
Aug 2, 2026 · 6 min read