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 (Java 9+) to whitelist allowed classes.
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 (Java 9+)
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 -> {
Class<?> clazz = filterInfo.serialClass();
// Allowlist safe classes
if (clazz != null) {
if (clazz == User.class ||
clazz == String.class ||
clazz == java.util.ArrayList.class) {
return ObjectInputFilter.Status.ALLOWED;
}
return ObjectInputFilter.Status.REJECTED;
}
// Reject anything not explicitly allowlisted
return ObjectInputFilter.Status.REJECTED;
});
return ois.readObject();
}
}
Why this works:
- Inspects every class before it is instantiated.
- Allowlists expected classes and rejects everything else.
- Blocks constructors and
readObject()before they run. - Applies to the full object graph, including nested types.
- Stops gadget chains by preventing dangerous types.
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.constructor.SafeConstructor;
Yaml yaml = new Yaml(new SafeConstructor());
Object obj = yaml.load(input); // Safe - only basic 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 (Java 9+) or replace with JSON
fastjson < v1.2.68
- Known RCE vulnerabilities
- Upgrade to latest version or replace with Jackson
Migration Recommendations
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())
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]
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;
}
}