Remote Code Execution via Untrusted Object Deserialization in Java RMI

Critical Risk Remote Code Execution
JavaRMIDeserializationObject InjectionRemote Code ExecutionSerialization

What it is

Java RMI methods that accept arbitrary serializable objects can be exploited through deserialization attacks, allowing attackers to execute arbitrary code on the server by crafting malicious serialized objects that trigger gadget chains during unmarshalling.

import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;

public interface DataProcessor extends Remote {
    String processUserData(Object userData) throws RemoteException;
    void handleRequest(Serializable request) throws RemoteException;
}

public class DataProcessorImpl extends UnicastRemoteObject implements DataProcessor {
    public DataProcessorImpl() throws RemoteException {
        super();
    }
    
    // Vulnerable: Accepts arbitrary objects for deserialization
    public String processUserData(Object userData) throws RemoteException {
        try {
            // Direct processing of untrusted object
            return userData.toString() + " processed";
        } catch (Exception e) {
            throw new RemoteException("Processing failed", e);
        }
    }
    
    // Vulnerable: Accepts serializable objects without validation
    public void handleRequest(Serializable request) throws RemoteException {
        // Deserialization happens automatically
        processRequest(request);
    }
    
    private void processRequest(Object request) {
        // Process the deserialized object
        System.out.println("Processing: " + request.getClass().getName());
    }
}
import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.*;

public interface DataProcessor extends Remote {
    String processUserData(String userDataJson) throws RemoteException;
    void handleRequest(String requestId, Map<String, String> params) throws RemoteException;
}

public class DataProcessorImpl extends UnicastRemoteObject implements DataProcessor {
    private final ObjectMapper objectMapper = new ObjectMapper();
    private final DataService dataService = new DataService();
    
    static {
        // Configure global deserialization filter
        System.setProperty("jdk.serialFilter", 
            "java.base/**;java.rmi.**;" +
            "!org.apache.commons.collections.**;" +
            "!org.springframework.**;" +
            "maxdepth=10;maxarray=10000");
    }
    
    public DataProcessorImpl() throws RemoteException {
        super();
    }
    
    // Secure: Uses JSON instead of object serialization
    public String processUserData(String userDataJson) throws RemoteException {
        try {
            // Parse JSON with schema validation
            UserData userData = objectMapper.readValue(userDataJson, UserData.class);
            
            // Validate the parsed data
            if (!isValidUserData(userData)) {
                throw new RemoteException("Invalid user data");
            }
            
            return processValidData(userData);
        } catch (Exception e) {
            throw new RemoteException("Processing failed", e);
        }
    }
    
    // Secure: Uses ID-based approach instead of object deserialization
    public void handleRequest(String requestId, Map<String, String> params) throws RemoteException {
        try {
            // Load request from trusted source using ID
            RequestData request = dataService.loadRequest(requestId);
            if (request == null) {
                throw new RemoteException("Request not found: " + requestId);
            }
            
            // Validate parameters
            validateParameters(params);
            
            processSecureRequest(request, params);
        } catch (Exception e) {
            throw new RemoteException("Request handling failed", e);
        }
    }
    
    private boolean isValidUserData(UserData userData) {
        return userData != null && 
               userData.getName() != null && 
               userData.getName().length() > 0 &&
               userData.getName().length() <= 255;
    }
    
    private void validateParameters(Map<String, String> params) {
        if (params == null) return;
        
        for (Map.Entry<String, String> entry : params.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            
            if (key == null || value == null) {
                throw new IllegalArgumentException("Null parameter key or value");
            }
            
            if (key.length() > 100 || value.length() > 1000) {
                throw new IllegalArgumentException("Parameter too long");
            }
        }
    }
    
    private String processValidData(UserData userData) {
        return "Processed user: " + userData.getName();
    }
    
    private void processSecureRequest(RequestData request, Map<String, String> params) {
        System.out.println("Processing secure request: " + request.getId());
    }
}

// Safe data transfer object
class UserData {
    private String name;
    private int age;
    
    // Getters and setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

💡 Why This Fix Works

The vulnerable version accepts arbitrary objects that can be exploited through deserialization attacks. The secure version uses JSON serialization and ID-based object loading to avoid deserialization vulnerabilities.

â„šī¸ Configuration Fix

Configuration changes required - see explanation below.

💡 Explanation

This example shows how attackers can exploit RMI deserialization vulnerabilities by sending malicious serialized objects that execute code during deserialization.

â„šī¸ Configuration Fix

Configuration changes required - see explanation below.

💡 Explanation

This secure implementation uses ObjectInputFilter to restrict deserialization and avoids accepting arbitrary objects, instead using primitives and validated strings.

Why it happens

RMI remote methods that accept Object or Serializable parameters without restrictions allow attackers to send malicious serialized objects.

Root causes

RMI Methods Accepting Arbitrary Objects

RMI remote methods that accept Object or Serializable parameters without restrictions allow attackers to send malicious serialized objects.

Preview example – JAVA
public interface RemoteService extends Remote {
    String processData(Object data) throws RemoteException; // Vulnerable
    void handleRequest(Serializable request) throws RemoteException; // Vulnerable
}

Missing Deserialization Controls

Applications that don't implement deserialization filters or input validation allow untrusted objects to be deserialized without restrictions.

Preview example – JAVA
// No ObjectInputFilter configured
public class RMIServerImpl extends UnicastRemoteObject implements RemoteService {
    public String processData(Object data) throws RemoteException {
        // Direct deserialization of untrusted data
        return processObject(data);
    }
}

Gadget Chain Exploitation

Applications with vulnerable libraries in classpath that contain gadget chains, allowing deserialization attacks to execute arbitrary code.

Preview example – JAVA
// Vulnerable when Apache Commons Collections, Spring, etc. are in classpath
public void handleData(HashMap<String, Object> payload) {
    // Malicious payload can contain gadget chain objects
    payload.forEach((key, value) -> {
        // Processing triggers deserialization vulnerability
        processValue(value);
    });
}

Fixes

1

Use Primitive Types and IDs Instead of Objects

Replace object parameters with primitive types, strings, or identifiers, then load the actual objects server-side from trusted sources.

2

Implement ObjectInputFilter (JEP 290)

Use Java's built-in ObjectInputFilter to restrict which classes can be deserialized, blocking dangerous classes and limiting object graph depth.

3

Use Safe Serialization Formats

Replace Java serialization with safer formats like JSON, Protocol Buffers, or Avro with proper schema validation.

4

Implement Custom Serialization with Validation

If object serialization is required, implement custom serialization methods with strict validation and whitelisting.

Detect This Vulnerability in Your Code

Sourcery automatically identifies remote code execution via untrusted object deserialization in java rmi and many other security issues in your codebase.