You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ratis.apache.org by sz...@apache.org on 2026/09/23 03:32:50 UTC

(ratis) branch master updated: RATIS-2700. Add a disallow-list for ObjectInputStream (#1596)

This is an automated email from the ASF dual-hosted git repository.

szetszwo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ratis.git


The following commit(s) were added to refs/heads/master by this push:
     new 82900ce56 RATIS-2700. Add a disallow-list for ObjectInputStream (#1596)
82900ce56 is described below

commit 82900ce565c349d2c952ac84875908b08d9d539d
Author: Tsz-Wo Nicholas Sze <sz...@apache.org>
AuthorDate: Tue Sep 22 20:32:45 2026 -0700

    RATIS-2700. Add a disallow-list for ObjectInputStream (#1596)
---
 .../main/java/org/apache/ratis/util/IOUtils.java   | 102 +++++++++++++++------
 .../java/org/apache/ratis/util/TestIOUtils.java    |  98 +++++++++++++++++++-
 2 files changed, 167 insertions(+), 33 deletions(-)

diff --git a/ratis-common/src/main/java/org/apache/ratis/util/IOUtils.java b/ratis-common/src/main/java/org/apache/ratis/util/IOUtils.java
index 302a45f85..93a33d9fd 100644
--- a/ratis-common/src/main/java/org/apache/ratis/util/IOUtils.java
+++ b/ratis-common/src/main/java/org/apache/ratis/util/IOUtils.java
@@ -39,6 +39,8 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.ExecutionException;
@@ -48,8 +50,13 @@ import java.util.function.Supplier;
 /**
  * IO related utility methods.
  */
-public interface IOUtils {
-  List<String> ALLOWED_PACKAGES_FOR_OBJECT_INPUT_STREAM = Collections.unmodifiableList(Arrays.asList(
+public final class IOUtils {
+  private IOUtils() {
+    throw new AssertionError("Not instantiable");
+  }
+
+  private static final List<String> ALLOWED_PREFIXES
+      = Collections.unmodifiableList(Arrays.asList(
       "java.lang.",
       "java.util.",
       "java.io.",
@@ -61,36 +68,80 @@ public interface IOUtils {
       "org.apache.ratis."
   ));
 
-  static void assertPackageForObjectInputStream(String fullClassName) throws InvalidClassException {
+  private static final Set<String> DISALLOWED_CLASSES;
+  static {
+    // all the classes below are subjected to java deserialization vulnerabilities
+    final List<String> disallowed = Arrays.asList(
+        // JRE Core Entry Points (Kick-Off Gadgets)
+        "java.util.HashMap",
+        "java.util.HashSet",
+        "java.util.LinkedHashSet",
+        "java.util.PriorityQueue",
+        "javax.management.BadAttributeValueExpException",
+        "java.beans.EventHandler",
+        "java.lang.ProcessBuilder",
+        "java.lang.Runtime",
+        "java.security.SignedObject",
+        "sun.reflect.annotation.AnnotationInvocationHandler",
+        // Intermediate & Proxy Gadgets
+        "java.util.TreeMap",
+        "java.util.TreeSet",
+        "java.lang.reflect.Proxy",
+        // Execution & Class Loading Sinks
+        "javax.xml.transform.Templates",
+        "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
+        "sun.rmi.server.MarshalInputStream",
+        // JNDI & Remote Triggers
+        "javax.naming.InitialContext",
+        "java.rmi.server.RemoteObject",
+        "java.rmi.server.RemoteObjectInvocationHandler",
+        "java.rmi.server.UnicastRef",
+        "com.sun.rowset.JdbcRowSetImpl",
+        // Network Reconnaissance & OOB Triggers
+        "java.net.URL"
+    );
+    DISALLOWED_CLASSES = Collections.unmodifiableSet(new TreeSet<>(disallowed));
+  }
+
+  static boolean isDisallowedForObjectInputStream(String fullClassName) {
+    return DISALLOWED_CLASSES.contains(fullClassName);
+  }
+
+  static void assertClassForObjectInputStream(String fullClassName) throws InvalidClassException {
+    if (isDisallowedForObjectInputStream(fullClassName)) {
+      throw new InvalidClassException(fullClassName, "Disallowed class");
+    }
+
     boolean isAllowed = false;
-    for (String allowed : ALLOWED_PACKAGES_FOR_OBJECT_INPUT_STREAM) {
+    for (String allowed : ALLOWED_PREFIXES) {
       if (fullClassName.startsWith(allowed)) {
         isAllowed = true;
+        break;
       }
     }
     if (!isAllowed) {
-      throw new InvalidClassException(fullClassName, "Disallowed package");
+      throw new InvalidClassException(fullClassName, "Not an allowed prefix");
     }
   }
 
-  static InterruptedIOException toInterruptedIOException(
+  public static InterruptedIOException toInterruptedIOException(
       String message, InterruptedException e) {
     final InterruptedIOException iioe = new InterruptedIOException(message);
     iioe.initCause(e);
     return iioe;
   }
 
-  static IOException asIOException(Throwable t) {
+  public static IOException asIOException(Throwable t) {
     Objects.requireNonNull(t, "t == null");
     return t instanceof IOException? (IOException)t : new IOException(t);
   }
 
-  static IOException toIOException(ExecutionException e) {
+  public static IOException toIOException(ExecutionException e) {
     final Throwable cause = e.getCause();
     return cause != null? asIOException(cause): new IOException(e);
   }
 
-  static <T> T getFromFuture(CompletableFuture<T> future, Supplier<Object> name) throws IOException {
+  public static <T> T getFromFuture(CompletableFuture<T> future, Supplier<Object> name) throws IOException {
     try {
       return future.get();
     } catch (InterruptedException e) {
@@ -103,7 +154,7 @@ public interface IOUtils {
     }
   }
 
-  static <T> T getFromFuture(CompletableFuture<T> future, Supplier<Object> name, TimeDuration timeout)
+  public static <T> T getFromFuture(CompletableFuture<T> future, Supplier<Object> name, TimeDuration timeout)
       throws IOException {
     try {
       return future.get(timeout.getDuration(), timeout.getUnit());
@@ -119,7 +170,7 @@ public interface IOUtils {
     }
   }
 
-  static boolean shouldReconnect(Throwable e) {
+  public static boolean shouldReconnect(Throwable e) {
     for (; e != null; e = e.getCause()) {
       if (ReflectionUtils.isInstance(e,
           SocketException.class, SocketTimeoutException.class, ClosedChannelException.class, EOFException.class,
@@ -130,13 +181,6 @@ public interface IOUtils {
     return false;
   }
 
-  static void readFully(InputStream in, int buffSize) throws IOException {
-    final byte [] buf = new byte[buffSize];
-    for(int bytesRead = in.read(buf); bytesRead >= 0; ) {
-      bytesRead = in.read(buf);
-    }
-  }
-
   /**
    * Reads len bytes in a loop.
    *
@@ -147,7 +191,7 @@ public interface IOUtils {
    * @throws IOException if it could not read requested number of bytes
    * for any reason (including EOF)
    */
-  static void readFully(InputStream in, byte[] buf, int off, int len)
+  public static void readFully(InputStream in, byte[] buf, int off, int len)
       throws IOException {
     for(int toRead = len; toRead > 0; ) {
       final int ret = in.read(buf, off, toRead);
@@ -160,7 +204,7 @@ public interface IOUtils {
     }
   }
 
-  static long preallocate(FileChannel fc, long size, ByteBuffer fill) throws IOException {
+  public static long preallocate(FileChannel fc, long size, ByteBuffer fill) throws IOException {
     Preconditions.assertSame(0, fill.position(), "fill.position");
     Preconditions.assertSame(fill.capacity(), fill.limit(), "fill.limit");
     final int remaining = fill.remaining();
@@ -185,7 +229,7 @@ public interface IOUtils {
    * @throws IOException if it could not skip requested number of bytes
    * for any reason (including EOF)
    */
-  static void skipFully(InputStream in, long len) throws IOException {
+  public static void skipFully(InputStream in, long len) throws IOException {
     long amt = len;
     while (amt > 0) {
       long ret = in.skip(amt);
@@ -210,7 +254,7 @@ public interface IOUtils {
    * @param log the log to record problems to at debug level. Can be null.
    * @param closeables the objects to close
    */
-  static void cleanup(Logger log, Closeable... closeables) {
+  public static void cleanup(Logger log, Closeable... closeables) {
     for (Closeable c : closeables) {
       if (c != null) {
         try {
@@ -225,16 +269,16 @@ public interface IOUtils {
   }
 
   /** Serialize the given object to a byte array using {@link java.io.ObjectOutputStream#writeObject(Object)}. */
-  static byte[] object2Bytes(Object obj) {
+  public static byte[] object2Bytes(Object obj) {
     return ProtoUtils.writeObject2ByteString(obj).toByteArray();
   }
 
-  static <T> T bytes2Object(byte[] bytes, Class<T> clazz) {
+  public static <T> T bytes2Object(byte[] bytes, Class<T> clazz) {
     return readObject(new ByteArrayInputStream(bytes), clazz);
   }
 
   /** @return an {@link ObjectInputStream}, which apply an allow-list filter. */
-  static ObjectInputStream newObjectInputStream(InputStream in) throws IOException {
+  public static ObjectInputStream newObjectInputStream(InputStream in) throws IOException {
     return new ObjectInputStream(in) {
       @Override
       protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
@@ -250,7 +294,7 @@ public interface IOUtils {
           throw new InvalidClassException(fullClassName, "Invalid class name");
         } else if (i == 0) {
           // not an array
-          assertPackageForObjectInputStream(fullClassName);
+          assertClassForObjectInputStream(fullClassName);
         } else if (i < last) {
           // i == last   : primitive array (no package to check)
           // 0 < i < last: non-primitive array
@@ -259,7 +303,7 @@ public interface IOUtils {
             throw new InvalidClassException(fullClassName, "Invalid array name");
           }
           // check array element class
-          assertPackageForObjectInputStream(fullClassName.substring(i+1, last));
+          assertClassForObjectInputStream(fullClassName.substring(i+1, last));
         }
 
         return super.resolveClass(desc);
@@ -276,12 +320,12 @@ public interface IOUtils {
    *
    * @param <T> The class type.
    */
-  static <T> T readObject(InputStream in, Class<T> clazz) {
+  public static <T> T readObject(InputStream in, Class<T> clazz) {
     final Object obj;
     try(ObjectInputStream oin = newObjectInputStream(in)) {
       obj = oin.readObject();
     } catch (IOException | ClassNotFoundException e) {
-      throw new IllegalStateException("Failed to readObject for class " + clazz, e);
+      throw new IllegalStateException("Failed to readObject for " + clazz, e);
     }
     try {
       return clazz.cast(obj);
diff --git a/ratis-common/src/test/java/org/apache/ratis/util/TestIOUtils.java b/ratis-common/src/test/java/org/apache/ratis/util/TestIOUtils.java
index 700b28fff..e3a56dd97 100644
--- a/ratis-common/src/test/java/org/apache/ratis/util/TestIOUtils.java
+++ b/ratis-common/src/test/java/org/apache/ratis/util/TestIOUtils.java
@@ -21,11 +21,29 @@ import org.apache.ratis.BaseTest;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.beans.EventHandler;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InvalidClassException;
+import java.io.NotSerializableException;
 import java.io.ObjectOutputStream;
+import java.lang.reflect.Proxy;
 import java.math.BigDecimal;
+import java.net.URL;
+import java.rmi.server.RemoteObject;
+import java.rmi.server.RemoteObjectInvocationHandler;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.PriorityQueue;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.function.Consumer;
+import javax.management.BadAttributeValueExpException;
+import javax.naming.InitialContext;
+import javax.xml.transform.Templates;
 
 /** Test methods of {@link IOUtils}. */
 public class TestIOUtils extends BaseTest {
@@ -50,19 +68,91 @@ public class TestIOUtils extends BaseTest {
   }
 
   @Test
-  public void testReadObjectRejectsDisallowedClass() throws IOException {
+  public void testAllowedList() throws Exception {
+    // BigDecimal is in java.math which is in neither the allow-list nor the disallow-list
+    final BigDecimal pi = new BigDecimal("3.14");
+    assertDisallowed(pi, false);
+
+    // Hide it inside a list
+    final List<Object> list = new ArrayList<>();
+    list.add("testing");
+    list.add(pi);
+    assertDisallowed(list, false);
+  }
+
+  /**
+   * Assert some of the JRE classes are in the disallow-list
+   * since these classes are subjected to Java deserialization vulnerabilities.
+   * When an object cannot be easily created or the class does not exist in all JRE implementations,
+   * test the full class name.
+   */
+  @Test
+  public void testDisallowedList() throws Exception {
+    // Kick-off & Entry Points (Sources)
+    assertDisallowed(new HashMap<>());
+    assertDisallowed(new HashSet<>());
+    assertDisallowed(new LinkedHashSet<>());
+    assertDisallowed(new PriorityQueue<>());
+    assertDisallowed(new BadAttributeValueExpException(null));
+    assertDisallowed(new EventHandler("object", "test", "event", "listener"));
+    assertDisallowed(new ProcessBuilder());
+    assertDisallowed(Runtime.getRuntime());
+    assertIsDisallowedForObjectInputStream("java.security.SignedObject"); //hard to create
+    assertIsDisallowedForObjectInputStream("sun.reflect.annotation.AnnotationInvocationHandler"); //package private
+
+    // Intermediate & Proxy Gadgets
+    assertDisallowed(new TreeMap<>());
+    assertDisallowed(new TreeSet<>());
+    assertDisallowed(Proxy.newProxyInstance(
+        Proxy.class.getClassLoader(),
+        new Class<?>[] { Consumer.class },
+        (proxy, method, args) -> null
+    ));
+
+    // Execution & Class Loading Sinks
+    assertIsDisallowedForObjectInputStream(Templates.class.getName()); //interface
+    assertIsDisallowedForObjectInputStream("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"); //not exist
+    assertIsDisallowedForObjectInputStream("sun.rmi.server.MarshalInputStream"); //class not exist
+
+    // JNDI & Remote Triggers
+    assertDisallowed(new InitialContext());
+    assertIsDisallowedForObjectInputStream(RemoteObject.class.getName()); //hard to create or MarshalException
+    assertIsDisallowedForObjectInputStream(RemoteObjectInvocationHandler.class.getName()); //hard to create
+    assertIsDisallowedForObjectInputStream("java.rmi.server.UnicastRef"); //class not exist
+    assertIsDisallowedForObjectInputStream("com.sun.rowset.JdbcRowSetImpl"); //class not exist
+
+    // Network Reconnaissance & OOB Triggers
+    assertDisallowed(new URL("http://localhost"));
+  }
+
+  static void assertDisallowed(Object object) throws Exception {
+    assertDisallowed(object, true);
+  }
+
+  static void assertDisallowed(Object object, boolean isInDisallowedList) throws Exception {
+    final Class<?> clazz = object.getClass();
     final ByteArrayOutputStream bout = new ByteArrayOutputStream();
     try (ObjectOutputStream oout = new ObjectOutputStream(bout)) {
-      oout.writeObject(new BigDecimal("3.14"));
+      oout.writeObject(object);
+    } catch (NotSerializableException e) {
+      // nothing to test since it is not serializable
+      return;
     }
-    final byte[] bytes = bout.toByteArray();
 
+    if (isInDisallowedList) {
+      assertIsDisallowedForObjectInputStream(clazz.getName());
+    }
+    final byte[] bytes = bout.toByteArray();
     final IllegalStateException ise = Assertions.assertThrows(IllegalStateException.class,
-        () -> IOUtils.bytes2Object(bytes, Integer.class));
+        () -> IOUtils.bytes2Object(bytes, clazz));
     // The cause should be an InvalidClassException raised by the filter.
     Throwable cause = ise.getCause();
     Assertions.assertNotNull(cause, "expected a cause on " + ise);
     Assertions.assertEquals(InvalidClassException.class, cause.getClass(),
         "unexpected cause: " + cause);
   }
+
+  static void assertIsDisallowedForObjectInputStream(String classname) {
+    Assertions.assertTrue(IOUtils.isDisallowedForObjectInputStream(classname), classname);
+  }
 }