Safeguard
Application Security

Java DTOs for secure data handling

A single @RequestBody bound to a JPA entity can let attackers set fields like isAdmin — DTOs close that gap by design, not by discipline.

Safeguard Research Team
Research
6 min read

In March 2012, security researcher Egor Homakov exploited a mass assignment flaw in GitHub's public-key upload form, gaining commit access to the Rails organization's own repository to prove a bug he'd been raising without success. The root cause was mundane: Rails happily bound every parameter in an incoming request onto an ActiveRecord model, including fields the form never intended to expose. GitHub patched it within hours and audited its codebase for the same pattern. That incident, now over a decade old, is still the canonical case study for a vulnerability class OWASP formally tracks as Mass Assignment — and it is just as reachable in a modern Spring Boot application as it was in 2012 Rails, wherever a controller method accepts a JSON body and binds it straight onto a JPA @Entity. In 2023, OWASP's API Security Top 10 folded Mass Assignment (API6:2019) together with Excessive Data Exposure (API3:2019) into a single category, API3:2023: Broken Object Property Level Authorization — recognizing that unchecked inbound binding and unchecked outbound serialization are the same missing control, just facing opposite directions. This post covers why Data Transfer Objects (DTOs) are the structural fix, not a coding-style preference.

What is mass assignment, concretely, in a Java web application?

Mass assignment happens when a framework takes every key in an incoming request body and automatically sets a matching field on a persistence object, without checking whether that field was supposed to be settable by the client at all. In Spring MVC or Spring Boot, this typically looks like a controller method annotated @PostMapping that accepts a @RequestBody User user parameter, where User is the same class annotated @Entity and mapped to the users table. Jackson, Spring's default JSON deserializer, will populate every field on User that appears in the incoming JSON and has a matching setter — including role, isAdmin, accountBalance, or verified, even if the legitimate client-facing form only ever displays name and email. An attacker who inspects the API (or just guesses field names from the entity's getters, which often leak in JSON responses) can add "role":"ADMIN" to a signup payload and have Spring set it for them, because nothing in the binding layer distinguishes "fields the UI shows" from "fields the wire format accepts."

Why doesn't validation alone (@Valid, @NotNull) fix this?

Bean Validation annotations like @NotNull, @Size, and @Email check that a field's value conforms to a constraint — they say nothing about whether the client should have been allowed to set that field in the first place. @Valid on a User entity will happily confirm that role is a valid, non-null enum value equal to ADMIN; it has no concept of "this field is server-assigned only." Validation and authorization operate on different axes: validation asks "is this a well-formed value," while mass-assignment protection asks "is this even a field this actor is allowed to write." A field can pass every @NotNull/@Pattern check on an entity and still represent a privilege escalation, because those annotations were designed for data integrity, not property-level access control — which is exactly the property-level authorization gap OWASP's API3:2023 category names directly.

How does a DTO structurally prevent this, instead of relying on developer discipline?

A DTO is a plain class containing only the fields a given operation is meant to accept or return — a UserSignupRequest DTO for a signup endpoint might declare only name, email, and password, with no role or isAdmin field present anywhere in the class. Because Jackson can only deserialize JSON into fields that exist on the target class, there is no setter for role to bind to, no matter what the attacker includes in the payload — the extra key is silently ignored rather than silently accepted. This flips the security model from a blocklist (remembering to guard every sensitive field on every entity, on every endpoint, forever) to an allowlist (a field simply doesn't exist to be set unless someone deliberately added it to that specific DTO). The same principle Rails eventually shipped as attr_accessible and strong parameters — an explicit, per-action whitelist of settable attributes — is what a narrowly-scoped Java DTO enforces at compile time, checked by the type system rather than by a developer remembering to add a guard clause.

Does the DTO pattern also stop data from leaking out, not just in?

Yes, and this is the "excessive data exposure" half of the same OWASP category. If a GET /users/{id} endpoint returns the User entity directly, Jackson serializes every field on that entity into the JSON response — including a bcrypt password hash, internal audit flags, a linked payment-provider customer ID, or fields relating to other users pulled in through a JPA relationship. Returning a UserResponse DTO instead means the response class only declares id, name, and email; there is no passwordHash field to accidentally serialize, because it was never copied onto the DTO in the first place. This matters even for internal or "trusted" clients, because API responses get logged, cached, and forwarded through more infrastructure than any one team tracks, and a field present on the wire today can end up in a log aggregator or a mobile app's local storage next year.

Isn't the Spring Framework aware of this?

The Spring team has discussed the problem directly at the framework level: a GitHub issue on the spring-framework repository (issue #18408) proposed a @NoBind-style annotation to mark domain fields as ineligible for automatic binding, precisely to close this class of bug without requiring every team to build separate request/response classes. That issue was ultimately closed as superseded rather than implemented, and no built-in @NoBind annotation ever shipped, which means teams binding @RequestBody directly onto @Entity classes today are relying on a mitigation the framework itself has discussed and declined to enforce natively, rather than one it ships out of the box. With no native @NoBind-equivalent on the roadmap, DTOs remain the community-recommended, dependency-free way to get the same guarantee: Spring's own reference documentation and widely cited Spring guides recommend separating web-layer request/response classes from JPA persistence entities for exactly this reason, independent of any single library or annotation.

What's the practical cost of adopting DTOs, and is it worth it?

The honest cost is boilerplate: every entity that's exposed over an API now needs a matching request DTO, response DTO, and a mapping step between them, typically handled with a mapping library such as MapStruct to avoid hand-writing repetitive getter/setter copying. For a service with a few dozen entities, that's a real, measurable amount of additional code and a mapping layer to maintain and test. The payoff is that the security property no longer depends on every developer remembering, on every new endpoint, to strip role or passwordHash before returning or accepting a request — it's structurally impossible to forget a field that was never declared on the DTO in the first place. Given that the underlying vulnerability class has been well-documented since at least 2012 and was significant enough for OWASP to elevate it into a top-level 2023 API risk category, the ongoing maintenance cost of a DTO layer is small next to the cost of a single mass-assignment incident reaching production.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.