A POJO class in Java is a plain object with fields, getters, and setters and no framework dependencies — and it is exactly that innocence that makes it a security liability the instant a library starts populating it from untrusted input. On its own a POJO does nothing dangerous. But POJOs almost never stay on their own: they are what JSON bodies deserialize into, what ORMs hydrate, and what request payloads bind to. Those bindings are where a harmless data holder turns into an attacker's foothold.
This guide is about that transition — what a POJO class in Java actually is, and the specific ways plain objects get weaponized once framework machinery starts writing to them without you watching.
What a POJO class in Java actually is
POJO stands for Plain Old Java Object. The term was coined by Martin Fowler, Rebecca Parsons, and Josh MacKenzie to describe an object that does not extend framework base classes, implement special interfaces, or carry framework annotations — just a normal Java class. A textbook example:
public class UserProfile {
private String username;
private String email;
private boolean admin;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public boolean isAdmin() { return admin; }
public void setAdmin(boolean admin) { this.admin = admin; }
}
Nothing here is unsafe by inspection. The risk is entirely about who gets to call those setters and where the data comes from.
Mass assignment: when the framework sets fields you did not intend
The most common POJO vulnerability is mass assignment. Frameworks that bind request data to objects — Spring MVC with @ModelAttribute, Jackson deserializing a JSON body — will set every field they can match a name to. Look again at that UserProfile class. It has an admin field with a public setter. If a controller binds a request body straight onto it, a user can send:
{ "username": "attacker", "email": "a@example.com", "admin": true }
and hand themselves administrative access, because the binder faithfully called setAdmin(true). The class did nothing wrong. The binding did.
The fix is to never bind untrusted input directly onto a domain object that carries authority-bearing fields. Use a dedicated input DTO that contains only the fields a client is allowed to set:
public class UserRegistrationRequest {
private String username;
private String email;
// no admin field — it cannot be set from the request at all
}
Then map the DTO to your domain object in code, setting privileged fields yourself. In Spring you can also restrict bindable fields explicitly with @InitBinder and setAllowedFields, but the DTO approach is cleaner because the dangerous field simply is not present to bind.
Deserialization: the POJO as an attack target
The sharper danger is deserialization, where a POJO is reconstructed from a serialized byte stream or a JSON document. Two flavors matter.
Native Java serialization (ObjectInputStream) is the notorious one. If your code deserializes attacker-controlled bytes into objects, gadget chains in libraries already on your classpath can be triggered during the deserialization process itself, leading to remote code execution — long before your application logic ever inspects the result. The guidance here is blunt: do not deserialize untrusted data with native Java serialization. Prefer data formats like JSON, and if you must accept serialized objects, use a strict allowlist filter (ObjectInputFilter, available since Java 9) to reject any class you did not explicitly permit.
JSON deserialization into POJOs is safer but not automatically safe. Libraries like Jackson have had polymorphic deserialization features (enableDefaultTyping, @JsonTypeInfo) that let the incoming document choose which concrete class to instantiate — and if an attacker controls that choice, they can steer instantiation toward gadget classes. This is the root of a long line of Jackson databind advisories. The Jackson databind security guide covers the specifics; the short version is: keep polymorphic typing off unless you truly need it, and when you do need it, restrict it to a known set of types rather than allowing arbitrary classes.
Getters, setters, and unexpected side effects
A POJO is supposed to have dumb accessors, but people occasionally put logic in setters — lazy initialization, validation, external calls. That turns every deserialization into an invocation of that logic with attacker-chosen values, at a point in the lifecycle where invariants may not hold yet. Keep POJO accessors free of side effects. If a field needs validation, validate after construction in a dedicated step you control, not inside a setter the deserializer calls in an order you do not.
Immutability helps here too. A POJO built through a constructor with final fields and no setters cannot be mutated by mass assignment after the fact, and gives the deserializer far less to work with. Java records, introduced as a standard feature in Java 16, are effectively immutable POJOs with a compact syntax and pair well with constructor-based JSON binding.
Validating POJOs at the trust boundary
Once data lands in a POJO from outside your system, treat the object as untrusted until validated. Bean Validation (Jakarta Validation, the jakarta.validation annotations) lets you declare constraints on the fields and enforce them at the boundary:
public class UserRegistrationRequest {
@NotBlank @Size(max = 64)
private String username;
@Email @NotBlank
private String email;
}
With @Valid on the controller parameter, the framework rejects a malformed payload before your logic runs. This does not replace the DTO discipline — it complements it. The DTO controls which fields can be set; validation controls what values they can hold.
Finding the risk across a real codebase
The trouble with POJO vulnerabilities is that they are patterns, not single lines — a setter here, a bind there, a polymorphic-typing flag in a config class somewhere else. Reviewing them by hand across a large service does not scale. Static analysis that understands framework binding and deserialization sinks will flag the dangerous combinations: domain objects bound directly to request bodies, enableDefaultTyping calls, native deserialization of external input. An SCA tool such as Safeguard adds the other half by flagging when a serialization library on your classpath has a known deserialization CVE, so you catch both your code patterns and the library versions that make them exploitable.
FAQ
Is a POJO the same as a JavaBean?
Not quite. Every JavaBean is a POJO, but a JavaBean adds conventions: a public no-argument constructor, private fields exposed through standard getters/setters, and serializability. A POJO has no required conventions — it is just a plain object with no framework coupling.
Are Java records safer than traditional POJOs?
Generally yes, for input handling. Records are immutable, constructed in one step, and have no setters, which eliminates mass-assignment-after-construction and reduces the mutable surface a deserializer can manipulate. They are a good default for request and response data holders.
Does using DTOs actually stop mass assignment?
Yes, when the DTO omits the sensitive fields entirely. If a field is not present on the object being bound, no binder can set it from a request. The privileged field only gets set later, in your own mapping code, from a trusted source.
Can JSON deserialization into a POJO cause remote code execution?
It can if polymorphic/default typing is enabled and lets attacker input choose which class to instantiate, steering toward gadget classes on the classpath. With polymorphic typing off or restricted to an allowlist of expected types, ordinary JSON-to-POJO binding is not an RCE vector.