What is the mass-assignment (over-posting) risk in Spring MVC data binding, and how do you defend against it?
answer
- Binder matches param name→property = over-posting
- Attacker adds role=ADMIN / id=1
- Best fix: DTO/command object, not the entity
- setAllowedFields (allow-list) > setDisallowedFields
- @RequestBody: Jackson DTO/@JsonIgnore, not InitBinder
basics
~20 s@ModelAttribute binds every request parameter whose name matches a property, so an attacker can add extra params (like admin=true) to set fields the form never exposed. Defend with setAllowedFields/setDisallowedFields in @InitBinder, or bind to a narrow DTO instead of the entity.
solid answer
~40 sWebDataBinder binds all request parameters whose names match target properties (including nested paths like address.city). If you bind directly to a domain entity, an attacker can 'over-post' parameters the UI never rendered — e.g. `role=ADMIN` or `id=1` — and silently set privileged fields: the mass-assignment vulnerability. Defenses: (1) bind to a purpose-built DTO/command object that only contains the fields a user may set, then map to the entity server-side — the strongest fix; (2) restrict the binder with `binder.setAllowedFields("name","email")` (allow-list, preferred) or `setDisallowedFields("id","role")` (deny-list) in an `@InitBinder`, optionally global via `@ControllerAdvice`; (3) never expose the JPA entity as the bind target. Note this is a @ModelAttribute concern; for @RequestBody use Jackson (`FAIL_ON_UNKNOWN_PROPERTIES`, `@JsonIgnore`, DTOs) since WebDataBinder allow-lists don't apply there.
code
java · 22 lines@ControllerAdvice
class BindingSecurityAdvice {
// global baseline: these must never be client-settable
@InitBinder
void baseline(WebDataBinder binder) {
binder.setDisallowedFields("id", "class.*", "role", "roles", "enabled");
}
}
@Controller
class UserController {
@InitBinder("userForm")
void allow(WebDataBinder binder) {
binder.setAllowedFields("name", "email"); // explicit allow-list
}
@PostMapping("/users")
String save(@ModelAttribute("userForm") UserForm form) {
// UserForm has no 'role'/'id' -> nothing sensitive to over-post
return "redirect:/users";
}
}go deeper
Understand that extra request params can set fields the form didn't show.
Name the fix: allow/deny lists in @InitBinder or a dedicated form/DTO object.
Argue allow-list over deny-list, cover nested-path binding and that @RequestBody needs Jackson-side defenses.
Establish an org-wide policy: never bind entities, DTOs by default, a global deny baseline in ControllerAdvice, and consistent Jackson strictness for JSON.
## The vulnerability `WebDataBinder` binds **by name matching**: for every request parameter, if there is a property with the same name (including nested `a.b.c` and indexed `list[0]` paths), it calls the setter. Convenient — and dangerous when the bind target is a rich object. ### Example of the attack ```java class User { String name; String email; String role; Long id; /* setters */ } @PostMapping("/users") String update(@ModelAttribute User user) { repo.save(user); return "ok"; } ``` The HTML form only shows `name` and `email`. An attacker submits: ``` POST /users name=Bob&[email protected]&role=ADMIN&id=1 ``` Because `role` and `id` match properties, the binder sets them. The user just made themselves an admin and/or overwrote another record. This is **mass assignment / over-posting** (OWASP: 'Mass Assignment'). ## Defenses (best to acceptable) ### 1. Bind to a DTO / command object (best) Expose only the fields the client may set: ```java class UpdateUserForm { private String name; private String email; /* getters/setters */ } @PostMapping("/users") String update(@ModelAttribute UpdateUserForm form, @AuthenticationPrincipal ...) { User u = repo.findById(currentId); u.setName(form.getName()); u.setEmail(form.getEmail()); repo.save(u); return "ok"; } ``` The binder simply has no `role`/`id` property to set. This also decouples the API from the entity. ### 2. Restrict the WebDataBinder ```java @InitBinder("user") void restrict(WebDataBinder binder) { binder.setAllowedFields("name", "email"); // allow-list (preferred) // or: binder.setDisallowedFields("id", "role", "*Password*"); } ``` - **Allow-list** is safer: new sensitive fields added later are excluded by default. - **Deny-list** patterns are case-insensitive and support `*` wildcards, but you must remember to blacklist every sensitive field — easy to miss. - Place in `@ControllerAdvice` for a global baseline (e.g. always disallow `id`, `class`, `role`). ### 3. Guard the classloader path Historically, binding could reach `class.classLoader...` (the basis of a notorious RCE). Modern Spring blocks `class.*` binding by default, but a global `setDisallowedFields("class.*")` is a defensive habit in older setups. ## @RequestBody is different Allow-lists in `@InitBinder` do **not** apply to `@RequestBody` — Jackson deserializes it. For JSON, defend with: - **DTOs** exposing only settable fields. - `@JsonIgnore` / read-only `@JsonProperty(access = READ_ONLY)` on sensitive fields. - `DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES` (Boot default true) so unexpected fields cause a 400 rather than being silently dropped (note: failing on unknowns is a strictness choice, not by itself full protection — a matching sensitive field still binds unless ignored). ## Gotchas - `setAllowedFields` failing silently: unlisted fields are ignored with no error, so a typo looks like the field 'not binding'. - Nested/indexed binding means allow-lists must include nested paths (`address.city`). - A global allow-list that's too strict breaks legitimate controllers — prefer a global **deny** baseline plus per-controller allow-lists, or DTOs everywhere.
- Why is binding to a JPA entity with @ModelAttribute considered risky even beyond mass assignment?Besides over-posting of privileged fields, it couples your HTTP contract to the persistence model, can trigger unintended lazy-loading/detached-entity issues, and makes it easy to accidentally persist attacker-controlled fields. A dedicated DTO/command object keeps the bindable surface minimal and intentional.
- Do setAllowedFields/setDisallowedFields protect a @RequestBody JSON handler?No. Those configure WebDataBinder, which @RequestBody doesn't use. For JSON, restrict via DTOs, @JsonIgnore / read-only access properties, and Jackson features like FAIL_ON_UNKNOWN_PROPERTIES on the ObjectMapper.
saying these in an interview costs you the question
- Binding directly to the JPA entity and calling it fine
- Believing @Valid prevents over-posting (it validates, it doesn't limit bindable fields)
- Thinking deny-lists are always sufficient (easy to miss a field)
- Assuming setAllowedFields also protects @RequestBody