DEV Community

Rocky
Rocky

Posted on

The Field Nobody Told You Was Writable: Mass Assignment in a Normal Update Request

A pentester is testing a self-service profile page. The endpoint is PATCH /api/users/me, and the form only exposes name and email, so that's what the request body normally carries. But an earlier GET /api/users/me call, pulled up in the same Burp history, returned a lot more than the form shows: role, isVerified, trialExpiresAt, discountPercent, fields the UI never renders anywhere. Out of habit more than expectation, they take that same PATCH request, add "role": "admin" to the body, and resend it.

No error. No 403. A clean 200, and the next GET confirms it: the account is now an admin.

That's mass assignment, sometimes called over-posting, and it's a genuinely different kind of bug from the ones most people cut their teeth on. There's no malformed syntax to smuggle past a filter, no payload to encode. The request is exactly as well-formed as a legitimate one. The bug is that a framework or ORM bound the entire incoming JSON body straight onto a data model, and nobody wrote down which of that model's fields a stranger on the internet is allowed to touch.

Finding it on purpose, rather than tripping over it, comes down to treating the object model as bigger than the form. Every authenticated response that returns your own user object, every error message that echoes a field name, an exposed OpenAPI or Swagger doc if one exists, all of it leaks the real shape of the record long before you touch the update endpoint. The move is to diff that full shape against whatever subset the form actually lets you set, then test the leftover fields one at a time against a legitimate request and check the value server-side afterward, not just the response code. A silent 200 that changes nothing is a dead end. A silent 200 that actually flips the field is the bug, and it looks identical to success either way until you go check.

Not every leftover field is worth chasing. The ones that matter are the ones that decide what you're allowed to do or see next: role, isAdmin, accountTier, verified, ownerId, teamId. Setting your own display name isn't interesting even if it's technically over-posting. Setting the field that gates authorization is a different conversation, and it's rarely the end of the conversation either. On its own it's a bug report. Chained with a second endpoint that trusts that field without re-checking it, it's account takeover or worse.

It's worth sitting with why this survives automated scanning as well as it does. A scanner tests known syntax patterns against known bug classes; it doesn't know your application's object model, so it has no way to know that role is the one field on this particular endpoint that should never come from the client. That knowledge only exists once a human reads the model and asks which fields are safe to expose, which is exactly the kind of trust-boundary reasoning that separates finding a known vulnerability class from finding the specific business-logic mistake this application made.

The fix, for what it's worth, is the mirror image of the bug: explicit allow-lists, not block-lists. A DTO or serializer that names precisely which fields a given endpoint accepts, never a raw bind of the whole request body onto a persistence model. It's not a hard fix. It's just one that has to be made deliberately, on every endpoint, which is exactly the kind of thing that gets skipped under a deadline.

This is the class of bug Codelivly's Advanced Web Application Penetration Testing Book - L2 is actually built around, 501 pages past the single-bug OWASP Top 10 into trust boundaries, business logic flaws and the chained attacks they enable, plus the secure code review side of catching them before an attacker does. If business logic testing isn't part of your process yet, the free Web Application Security learning path on codelivly.com is a solid place to build the habit of reading the object model before you read the form.

Top comments (0)