RMI Without Serialization Filters
Running RMI servers without ObjectInputFilter, allowing deserialization of arbitrary classes from remote clients.
Remote code execution vulnerabilities occur when RMI methods accept non-primitive object parameters that trigger deserialization of untrusted data, enabling gadget chain exploitation for arbitrary code execution.
// Remote interfacepublic interface DataService extends Remote { // VULNERABLE: Accepting complex object parameters String processData(UserData data) throws RemoteException; void updateConfiguration(Properties config) throws RemoteException; List<String> queryResults(SearchCriteria criteria) throws RemoteException;}// Implementationpublic class DataServiceImpl implements DataService { @Override public String processData(UserData data) throws RemoteException { // VULNERABLE: Object deserialization happens automatically return "Processed data for user: " + data.getUsername(); } @Override public void updateConfiguration(Properties config) throws RemoteException { // VULNERABLE: Properties object can contain dangerous payloads System.getProperties().putAll(config); } @Override public List<String> queryResults(SearchCriteria criteria) throws RemoteException { // VULNERABLE: SearchCriteria deserialization can trigger gadgets return Arrays.asList("result1", "result2"); }}// RMI Server setuppublic class RMIServer { public static void main(String[] args) throws Exception { // VULNERABLE: No deserialization filtering DataService service = new DataServiceImpl(); DataService stub = (DataService) UnicastRemoteObject.exportObject(service, 0); Registry registry = LocateRegistry.createRegistry(1099); registry.bind("DataService", stub); System.out.println("RMI Server started"); }}// SECURE: Use primitive parameters and IDspublic interface SecureDataService extends Remote { // SECURE: Only primitive and safe types String processDataById(long userId, String dataType) throws RemoteException; void updateConfigurationValue(String key, String value) throws RemoteException; List<String> queryResultsByJson(String jsonCriteria) throws RemoteException;}// Secure implementationpublic class SecureDataServiceImpl implements SecureDataService { private final UserRepository userRepository = new UserRepository(); private final ObjectMapper objectMapper = new ObjectMapper(); @Override public String processDataById(long userId, String dataType) throws RemoteException { // SECURE: Fetch data server-side using primitive ID UserData data = userRepository.findById(userId); if (data != null) { return "Processed " + dataType + " for user: " + data.getUsername(); } return "User not found"; } @Override public void updateConfigurationValue(String key, String value) throws RemoteException { // SECURE: Validate individual key-value pairs if (isValidConfigKey(key) && isValidConfigValue(value)) { System.setProperty(key, value); } else { throw new RemoteException("Invalid configuration parameter"); } } @Override public List<String> queryResultsByJson(String jsonCriteria) throws RemoteException { try { // SECURE: Parse JSON instead of deserializing objects SearchCriteria criteria = objectMapper.readValue(jsonCriteria, SearchCriteria.class); return performQuery(criteria); } catch (Exception e) { throw new RemoteException("Invalid search criteria", e); } } private boolean isValidConfigKey(String key) { // Allowlist of valid configuration keys Set<String> allowedKeys = Set.of("app.timeout", "app.maxConnections", "app.debug"); return allowedKeys.contains(key); } private boolean isValidConfigValue(String value) { // Basic validation for configuration values return value != null && value.length() < 100 && !value.contains("<script>"); } private List<String> performQuery(SearchCriteria criteria) { // Perform safe query based on validated criteria return Arrays.asList("result1", "result2"); }}// SECURE: RMI Server with deserialization filtering (JEP 290)public class SecureRMIServer { public static void main(String[] args) throws Exception { // SECURE: Set up deserialization filter System.setProperty("jdk.serialFilter", createSecureSerialFilter()); // Alternative: Programmatic filter ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(createSecureSerialFilter()); ObjectInputFilter.Config.setSerialFilter(filter); SecureDataService service = new SecureDataServiceImpl(); SecureDataService stub = (SecureDataService) UnicastRemoteObject.exportObject(service, 0); Registry registry = LocateRegistry.createRegistry(1099); registry.bind("SecureDataService", stub); System.out.println("Secure RMI Server started with deserialization filtering"); } private static String createSecureSerialFilter() { return "java.lang.String;" + "java.lang.Number;" + "java.util.List;" + "java.util.ArrayList;" + "com.example.safe.SearchCriteria;" + "!*"; // Reject all other classes }}// SECURE: Alternative approach with custom socket factoriespublic class FilteredRMIServer { public static void main(String[] args) throws Exception { // SECURE: Use custom socket factory with filtering RMIServerSocketFactory serverFactory = new FilteredServerSocketFactory(); RMIClientSocketFactory clientFactory = new FilteredClientSocketFactory(); SecureDataService service = new SecureDataServiceImpl(); SecureDataService stub = (SecureDataService) UnicastRemoteObject.exportObject( service, 0, clientFactory, serverFactory); Registry registry = LocateRegistry.createRegistry(1099, clientFactory, serverFactory); registry.bind("FilteredDataService", stub); System.out.println("RMI Server with filtered sockets started"); }}// Custom server socket factory with deserialization protectionclass FilteredServerSocketFactory implements RMIServerSocketFactory { @Override public ServerSocket createServerSocket(int port) throws IOException { return new ServerSocket(port) { @Override public Socket accept() throws IOException { Socket socket = super.accept(); return new FilteredSocket(socket); } }; }}class FilteredSocket extends Socket { private final Socket delegate; public FilteredSocket(Socket delegate) { this.delegate = delegate; } @Override public InputStream getInputStream() throws IOException { return new ObjectInputStream(delegate.getInputStream()) { @Override protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException { ObjectStreamClass desc = super.readClassDescriptor(); if (!isAllowedClass(desc.getName())) { throw new InvalidClassException("Class not allowed: " + desc.getName()); } return desc; } }; } private boolean isAllowedClass(String className) { Set<String> allowedClasses = Set.of( "java.lang.String", "java.lang.Integer", "java.lang.Long", "com.example.safe.SearchCriteria" ); return allowedClasses.contains(className); }}The vulnerable code was updated to address the security issue.
Running RMI servers without ObjectInputFilter, allowing deserialization of arbitrary classes from remote clients.
Sourcery automatically identifies remote code execution via rmi deserialization and many other security issues in your codebase.