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);
+ }
}