CWE-502: Insecure Deserialization - Java
Overview
Insecure deserialization occurs when untrusted data is used to create objects, potentially allowing attackers to execute arbitrary code, manipulate application logic, or achieve denial of service. Java's native serialization is particularly dangerous because it can invoke methods during deserialization.
Primary Defence: Use JSON (Jackson, Gson) instead of Java serialization, or if Java serialization is required, implement ObjectInputFilter to allowlist expected classes and enforce object graph limits.
Common Vulnerable Patterns
ObjectInputStream with Untrusted Data
// VULNERABLE - Deserializing user input
import java.io.*;
public class UserController {
public User loadUser(byte[] userData) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(userData);
ObjectInputStream ois = new ObjectInputStream(bis);
return (User) ois.readObject(); // DANGEROUS!
}
}
Why this is vulnerable:
- Deserializes arbitrary serializable classes from untrusted data.
- Invokes
readObject()/readResolve()and other callbacks automatically. - Gadget chains can execute code during object creation.
- Occurs before application-level validation or checks.
Reading Serialized Objects from Network
// VULNERABLE - Socket deserialization
import java.io.*;
import java.net.*;
public class Server {
public void handleClient(Socket client) throws Exception {
ObjectInputStream ois = new ObjectInputStream(client.getInputStream());
Object obj = ois.readObject(); // Can execute malicious code!
processObject(obj);
}
}
Why this is vulnerable:
- Remote attackers fully control the serialized bytes.
- Deserialization happens before any application validation.
- Gadget chains can execute code on the server.
- Common RCE vector for exposed services.
Deserializing from Files Without Validation
// VULNERABLE - File deserialization
public Object loadFromFile(String filename) throws Exception {
FileInputStream fis = new FileInputStream(filename);
ObjectInputStream ois = new ObjectInputStream(fis);
return ois.readObject(); // Attacker can control file content
}
Why this is vulnerable:
- File contents can be attacker-controlled (upload/traversal/shared).
- Deserialization trusts file data without validation.
- Gadget chains can execute during object creation.
- Executes with the application's privileges.
Using readObject() with Custom Serialization
// VULNERABLE - Custom readObject can be exploited
public class VulnerableClass implements Serializable {
private void readObject(ObjectInputStream ois) throws Exception {
ois.defaultReadObject();
// This code runs during deserialization!
Runtime.getRuntime().exec(someCommand); // RCE!
}
}
Why this is vulnerable:
readObject()runs automatically during deserialization.- Attackers can control the data that reaches this code.
- Enables command execution or other side effects.
- Triggered without explicit method calls.
Secure Patterns
Use JSON Instead of Java Serialization
// SECURE - JSON has no code execution
import com.fasterxml.jackson.databind.ObjectMapper;
public class UserController {
private final ObjectMapper objectMapper = new ObjectMapper();
public User loadUser(String jsonData) throws Exception {
// JSON deserialization doesn't execute code
return objectMapper.readValue(jsonData, User.class);
}
public String saveUser(User user) throws Exception {
return objectMapper.writeValueAsString(user);
}
}
Why this works:
- Requires an explicit target class (
User.class). - No Java serialization callbacks like
readObject(). - No arbitrary type instantiation by default.
- Input is treated as data-only primitives/objects.
- Default typing is off unless explicitly enabled.
Maven Dependency:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
Look-Ahead Deserialization with ObjectInputFilter
// SECURE - Allowlist allowed classes and limit object graph size
import java.io.*;
public class SafeDeserializer {
public Object deserialize(byte[] data) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bis);
// Set filter to allow only specific classes
ois.setObjectInputFilter(filterInfo -> {
if (filterInfo.depth() > 10 ||
filterInfo.references() > 1000 ||
filterInfo.arrayLength() > 10000 ||
filterInfo.streamBytes() > 1_000_000) {
return ObjectInputFilter.Status.REJECTED;
}
Class<?> clazz = filterInfo.serialClass();
// Allowlist safe classes
if (clazz != null) {
if (clazz.isArray()) {
return ObjectInputFilter.Status.UNDECIDED;
}
if (clazz == User.class ||
clazz == String.class ||
clazz == java.util.ArrayList.class) {
return ObjectInputFilter.Status.ALLOWED;
}
return ObjectInputFilter.Status.REJECTED;
}
return ObjectInputFilter.Status.UNDECIDED;
});
return ois.readObject();
}
}
Why this works:
- Inspects every class before it is instantiated.
- Allowlists expected classes and rejects everything else.
- Blocks dangerous class resolution and deserialization callbacks such as
readObject()/readResolve()before they process attacker-controlled state. - Applies to the full object graph, including nested types.
- Enforces graph limits to reduce memory and recursion denial-of-service risk.
- Stops gadget chains by preventing dangerous types.
Version note: ObjectInputFilter was introduced by JEP 290 and is available in Java 9+ and later Java 8 update releases. Use it where available; use a validating ObjectInputStream wrapper only for older runtimes that cannot be upgraded.
ValidatingObjectInputStream
// SECURE - Apache Commons IO validator (before Java 9)
import org.apache.commons.io.serialization.ValidatingObjectInputStream;
public class SecureDeserializer {
public Object deserialize(byte[] data) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(data);
ValidatingObjectInputStream vois = new ValidatingObjectInputStream(bis);
// Accept only allowlisted classes
vois.accept(User.class);
vois.accept(java.lang.String.class);
vois.accept(java.util.ArrayList.class);
// Reject dangerous patterns
vois.reject(java.lang.Runtime.class);
vois.reject(java.lang.ProcessBuilder.class);
vois.reject("org.apache.commons.collections.functors.*");
return vois.readObject();
}
}
Why this works:
- Allowlists classes or patterns via
accept(). - Rejects unknown classes before instantiation.
- Hooks
resolveClass()to block dangerous types early. - Works as a pre-Java-9 alternative to filters.
- Supports explicit
reject()for known gadget packages.
Maven Dependency:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.13.0</version>
</dependency>
Use Protocol Buffers or MessagePack
// SECURE - Protocol Buffers (no code execution)
import com.google.protobuf.InvalidProtocolBufferException;
public class UserController {
public UserProto.User loadUser(byte[] data) throws InvalidProtocolBufferException {
return UserProto.User.parseFrom(data);
}
public byte[] saveUser(UserProto.User user) {
return user.toByteArray();
}
}
Why this works:
- Schema defines the only allowed message structure.
parseFrom()targets a single message class.- No type metadata or polymorphic instantiation.
- No serialization callbacks like
readObject(). - Rejects malformed input outside the schema.
Maven Dependencies:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.24.0</version>
</dependency>
Java Library Safety Matrix
When reviewing code, use this matrix to identify unsafe deserialization libraries:
Safer Defaults (still validate your config)
jackson-databind (JSON)
- Safe UNLESS polymorphism enabled (
@JsonTypeInfo,enableDefaultTyping()) - Default configuration is secure
- Only deserializes to specified types
Kryo v5.0.0+
- Treat as unsafe unless registration is explicitly required
- Set
setRegistrationRequired(true)and register allowed classes
XStream v1.4.17+
- Requires explicit allowlisting via the security framework
- Earlier versions allow arbitrary class instantiation
fastjson2
- Safe if autotype feature NOT enabled
- Use latest version with security patches
Requires Configuration (UNSAFE by default)
Kryo < v5.0.0
- Requires
setRegistrationRequired(true):
Kryo kryo = new Kryo();
kryo.setRegistrationRequired(true); // MUST SET THIS
// Register only allowed classes
kryo.register(User.class);
kryo.register(Address.class);
fastjson v1.2.68+
- Requires safemode:
json-io
- Must use non-typed mode or custom deserializer
- Avoid using with untrusted data
SnakeYAML
- Must use SafeConstructor:
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.constructor.SafeConstructor;
Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()));
Object obj = yaml.load(input); // Safe - only basic types
For older SnakeYAML 1.x versions, new SafeConstructor() may be available without LoaderOptions, but the security requirement is the same: use the safe constructor or Yaml configuration that restricts YAML to basic data types.
CANNOT Be Used Safely (Replace immediately)
XMLDecoder (JDK)
- No safe configuration exists
- Allows arbitrary code execution
- Replace with JSON or safe XML parser
XStream < v1.4.17
- Allows arbitrary class instantiation
- Known RCE vulnerabilities
- Upgrade to v1.4.17+ or replace with JSON
Castor
- Abandoned project, no security updates
- Replace with modern serialization library
ObjectInputStream.readObject() without filters
- Default Java serialization is unsafe
- Use ObjectInputFilter where available or replace with JSON
fastjson < v1.2.68
- Known RCE vulnerabilities
- Upgrade to latest version or replace with Jackson
Migration Considerations
If you find these patterns in security scan results:
- ObjectInputStream.readObject() → Switch to JSON with Jackson
- XMLDecoder → Switch to JAXB with known types or JSON
- XStream < v1.4.17 → Upgrade or switch to JSON
- Kryo without registration → Enable
setRegistrationRequired(true) - SnakeYAML with Yaml() → Use
new Yaml(new SafeConstructor(new LoaderOptions()))on SnakeYAML 2.x, or the equivalent SafeConstructor configuration for your version
Framework-Specific Guidance
Spring Framework
// SECURE - Use Spring's JSON serialization
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
@RestController
public class UserController {
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
// Spring automatically deserializes JSON to User
// No dangerous ObjectInputStream involved
userService.save(user);
return ResponseEntity.ok(user);
}
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
// Spring serializes to JSON automatically
return ResponseEntity.ok(user);
}
}
// Configure Jackson to avoid polymorphic deserialization for untrusted input
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
// Keep default typing disabled for untrusted data
mapper.deactivateDefaultTyping();
return mapper;
}
}
Java EE / Jakarta EE
// SECURE - Use JSON-B (Jakarta JSON Binding)
import jakarta.json.bind.*;
public class UserService {
private final Jsonb jsonb = JsonbBuilder.create();
public User deserialize(String json) {
return jsonb.fromJson(json, User.class);
}
public String serialize(User user) {
return jsonb.toJson(user);
}
}
// JAX-RS with JSON
@Path("/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserResource {
@POST
public Response createUser(User user) {
// JSON deserialization handled by JAX-RS provider
userService.save(user);
return Response.ok(user).build();
}
}
Signature Verification
// SECURE - Verify HMAC before deserializing
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
public class SignedDeserializer {
private final SecretKey key;
public SignedDeserializer(byte[] secretKey) {
this.key = new SecretKeySpec(secretKey, "HmacSHA256");
}
public Object deserialize(byte[] signedData) throws Exception {
// Format: [signature][data]
if (signedData.length < 32) {
throw new SecurityException("Invalid signed data");
}
byte[] signature = new byte[32]; // SHA-256 is 32 bytes
byte[] data = new byte[signedData.length - 32];
System.arraycopy(signedData, 0, signature, 0, 32);
System.arraycopy(signedData, 32, data, 0, data.length);
// Verify signature
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
byte[] expectedSignature = mac.doFinal(data);
if (!MessageDigest.isEqual(signature, expectedSignature)) {
throw new SecurityException("Invalid signature");
}
// Only deserialize if signature is valid
// (But still prefer JSON over ObjectInputStream!)
return new ObjectMapper().readValue(data, Object.class);
}
}
Input Validation
// Validate after deserializing data-only formats (JSON/JSON-B)
import javax.validation.*;
import javax.validation.constraints.*;
public class User {
@NotNull
@Size(min = 1, max = 100)
private String username;
@Email
private String email;
@Min(0)
@Max(150)
private Integer age;
// getters and setters
}
// Controller with validation
@RestController
public class UserController {
private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
@PostMapping("/users")
public ResponseEntity<?> createUser(@Valid @RequestBody User user) {
Set<ConstraintViolation<User>> violations = validator.validate(user);
if (!violations.isEmpty()) {
return ResponseEntity.badRequest().body(violations);
}
userService.save(user);
return ResponseEntity.ok(user);
}
}
Detecting Gadget Chains
// Block known gadget chain classes
import java.io.*;
public class GadgetBlocker implements ObjectInputFilter {
private static final String[] BLOCKED_CLASSES = {
"org.apache.commons.collections.functors.InvokerTransformer",
"org.apache.commons.collections.functors.InstantiateTransformer",
"org.apache.commons.collections4.functors.InvokerTransformer",
"org.apache.commons.collections4.functors.InstantiateTransformer",
"org.codehaus.groovy.runtime.ConvertedClosure",
"org.codehaus.groovy.runtime.MethodClosure",
"org.springframework.beans.factory.ObjectFactory",
"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"java.rmi.server.UnicastRemoteObject",
"java.rmi.server.RemoteObjectInvocationHandler"
};
@Override
public Status checkInput(FilterInfo filterInfo) {
Class<?> clazz = filterInfo.serialClass();
if (clazz != null) {
String className = clazz.getName();
for (String blocked : BLOCKED_CLASSES) {
if (className.startsWith(blocked)) {
return Status.REJECTED;
}
}
}
return Status.UNDECIDED;
}
}
Known-gadget blocklists are useful only as temporary hardening around legacy Java serialization. They are incomplete by nature and should not replace ObjectInputFilter allowlists, graph limits, dependency reduction, and migration to data-only formats.
Remediation Steps
- Locate every deserialization sink, including
ObjectInputStream, RMI/JMX/remoting endpoints, HTTP session replication, message consumers, caches, and framework converters. - Determine whether serialized data can be supplied or modified across a trust boundary through files, queues, network requests, databases, or other services.
- Replace Java native serialization with JSON, JSON-B, Protocol Buffers, or another data-only format where possible.
- If Java serialization remains temporarily, configure
ObjectInputFilterwith explicit class allowlists plus limits for depth, references, array length, and stream bytes. - Remove high-risk gadget libraries and stale dependencies from the runtime classpath where they are not required.
- Validate the resulting data object after safe parsing, and keep signing or encryption as integrity controls rather than as the primary deserialization defense.
Testing
- Test normal JSON or data-only payloads and confirm validation accepts expected objects.
- Test native Java serialization payloads and confirm untrusted endpoints reject them before object construction.
- Test filter limits for object depth, reference count, array length, and total bytes.
- Test unexpected classes, proxy classes, and known gadget payloads in a controlled environment.
- Test tampered signed payloads and truncated payloads so integrity checks fail closed.
- Re-run static analysis and dependency scans for
ObjectInputStream, permissive YAML/XML mappers, and gadget-prone libraries.
Common Pitfalls
- Relying on a gadget blocklist as the primary defense.
- Adding validation after
readObject()has already constructed attacker-controlled objects. - Allowing broad package prefixes such as
com.company.*when only a few DTO classes are expected. - Forgetting graph limits; class filters alone do not prevent memory or CPU exhaustion.
- Signing serialized objects and then treating them as safe for long-term external input.
- Replacing Java serialization in controllers while leaving message queues, caches, or RMI endpoints unchanged.
Dependencies and Installation
ObjectInputFilteris available in Java 9+ and later Java 8 update releases through JEP 290.- Jackson, Gson, JSON-B, and Protocol Buffers are safer alternatives when used as data-only formats without polymorphic type loading from untrusted input.
- SnakeYAML 2.x safe loading requires
SafeConstructor(new LoaderOptions()); verify constructor APIs when maintaining older 1.x code. - Keep gadget-prone libraries current and remove unused libraries from the runtime classpath.