# Mass Assignment: The One-Line API Bug Hiding in Your Update Endpoint

> Source: <https://dev.to/anushadirisala/mass-assignment-the-one-line-api-bug-hiding-in-your-update-endpoint-4e7>
> Published: 2026-10-07 14:12:34+00:00

This article was created with the help of AI and reviewed, tested and edited by me before publishing.

Here's an endpoint that looks completely fine in code review:

``` js
app.patch('/api/users/me', requireAuth, async (req, res) => {
  const user = await User.findByIdAndUpdate(req.user.id, req.body, { new: true });
  res.json(user);
});
```

It's authenticated. It only updates the logged-in user. It's also vulnerable, because `req.body` goes straight into the database. If your `User` model has a `role` field, any user can send this:

```
PATCH /api/users/me
Content-Type: application/json

{ "displayName": "Ana", "role": "admin" }
```

and promote themselves. That's **mass assignment**: the client sets object properties it was never meant to touch. OWASP folds it into API3:2023, [Broken Object Property Level Authorization](https://owasp.org/API-Security/editions/2023/en/0xa3-broken-object-property-level-authorization/), and MITRE tracks it as [CWE-915](https://cwe.mitre.org/data/definitions/915.html).

Let's find it, prove it, and fix it.

Frameworks make binding request data to models easy, which is great until the model grows. The endpoint above might have been safe on day one, when `User` only had `displayName` and `email`. Then someone added `role`, `isVerified`, `credits` or `tenantId`, and the update endpoint silently started accepting them too.

Typical sensitive fields to watch for:

`role`, `isAdmin`, `permissions`
`isVerified`, `emailConfirmed`, `status`
`balance`, `credits`, `plan`, `discount`
`ownerId`, `tenantId`, `orgId`
`GET /api/users/me` that returns `GET` (or a behaviour change, like suddenly reaching an admin route) proves the write happened.`POST /api/projects` with an extra `{ "profile": { "verified": true } }` slips past filters that only check top-level keys.
Only do this on your own apps or targets you're authorised to test.

The [OWASP Mass Assignment Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Mass_Assignment_Cheat_Sheet.html) recommends allowlisting bindable fields over blocklisting dangerous ones, because a blocklist breaks the moment a new sensitive field is added.

``` js
const pick = (obj, keys) =>
  Object.fromEntries(keys.filter((k) => k in obj).map((k) => [k, obj[k]]));

const USER_EDITABLE = ['displayName', 'bio', 'avatarUrl'];

app.patch('/api/users/me', requireAuth, async (req, res) => {
  const updates = pick(req.body, USER_EDITABLE);
  const user = await User.findByIdAndUpdate(req.user.id, updates, { new: true });
  res.json(toPublicUser(user));
});
```

A schema validator that rejects unknown keys turns silent acceptance into a clear `400`. With [zod](https://zod.dev/):

``` js
import { z } from 'zod';

const UpdateMe = z.object({
  displayName: z.string().min(1).max(80).optional(),
  bio: z.string().max(500).optional(),
}).strict(); // unknown keys like "role" fail validation

app.patch('/api/users/me', requireAuth, async (req, res) => {
  const parsed = UpdateMe.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: 'Invalid fields' });
  const user = await User.findByIdAndUpdate(req.user.id, parsed.data, { new: true });
  res.json(toPublicUser(user));
});
```

Rejecting is better than silently dropping: clients find out quickly, and an attempted `role` change shows up in your logs.

API3 covers both directions. The `toPublicUser` function above matters because returning the raw model leaks internal fields (password hashes, internal flags) and hands attackers the list of fields to try. Use explicit response DTOs, not `res.json(dbObject)`.

The bug comes back whenever someone adds a field, so make it a regression test:

``` js
test('users cannot change their own role', async () => {
  const agent = await loginAs('regular-user');
  await agent.patch('/api/users/me').send({ role: 'admin' }).expect(400);

  const me = await agent.get('/api/users/me').expect(200);
  expect(me.body.role).toBeUndefined(); // not exposed at all
});
```

Add one of these for every sensitive field and every endpoint that writes to the model, including create endpoints and admin-only ones (can a support role set `role: "owner"`?).

`req.body` to an ORM update or create`400`, not silently ignored
Mass assignment is rarely clever. It's a convenience left in place after the model changed, which is exactly why it's worth a test that never forgets.

*Anusha Dirisala is the founder of Pentrova (pentrova.ai), a self-serve AI penetration testing platform for web apps and APIs, based in Hyderabad.*
