diff --git a/src/com/esotericsoftware/kryo/serializers/JavaSerializer.java b/src/com/esotericsoftware/kryo/serializers/JavaSerializer.java index bc8a5d6a7..a44760d3e 100644 --- a/src/com/esotericsoftware/kryo/serializers/JavaSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/JavaSerializer.java @@ -32,6 +32,7 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.ObjectStreamClass; +import java.util.function.Predicate; /** Serializes objects using Java's built in serialization mechanism. Note that this is very inefficient and should be avoided if * possible. @@ -40,6 +41,22 @@ * @see KryoSerializable * @author Nathan Sweet */ public class JavaSerializer extends Serializer { + private Predicate classFilter; + + /** Sets an optional filter applied to the name of each class encountered while deserializing. When set, a class whose name the + * predicate rejects is refused with a {@link KryoException} before the class is resolved, so an unwanted class is never loaded + * and a name that does not resolve at all is still refused. This is opt-in, defense-in-depth protection for reading serialized + * data from an untrusted source. When null (the default) no filtering is applied and behavior is unchanged. + * @param classFilter May be null. */ + public void setClassFilter (Predicate classFilter) { + this.classFilter = classFilter; + } + + /** @return May be null. */ + public Predicate getClassFilter () { + return classFilter; + } + public void write (Kryo kryo, Output output, Object object) { try { ObjectMap graphContext = kryo.getGraphContext(); @@ -60,7 +77,7 @@ public Object read (Kryo kryo, Input input, Class type) { ObjectMap graphContext = kryo.getGraphContext(); ObjectInputStream objectStream = (ObjectInputStream)graphContext.get(this); if (objectStream == null) { - objectStream = new ObjectInputStreamWithKryoClassLoader(input, kryo); + objectStream = new ObjectInputStreamWithKryoClassLoader(input, kryo, classFilter); graphContext.put(this, objectStream); } return objectStream.readObject(); @@ -75,13 +92,20 @@ public Object read (Kryo kryo, Input input, Class type) { * https://issues.apache.org/jira/browse/GROOVY-1627 */ private static class ObjectInputStreamWithKryoClassLoader extends ObjectInputStream { private final Kryo kryo; + private final Predicate classFilter; - ObjectInputStreamWithKryoClassLoader (InputStream in, Kryo kryo) throws IOException { + ObjectInputStreamWithKryoClassLoader (InputStream in, Kryo kryo, Predicate classFilter) throws IOException { super(in); this.kryo = kryo; + this.classFilter = classFilter; } protected Class resolveClass (ObjectStreamClass type) { + // Checked on the name, before the class is resolved: a rejected class is never loaded, and a name that + // does not resolve at all is still refused rather than reported as missing. + if (classFilter != null && !classFilter.test(type.getName())) { + throw new KryoException("Deserialization is not allowed for class: " + type.getName()); + } try { return Class.forName(type.getName(), false, kryo.getClassLoader()); } catch (ClassNotFoundException ignored) {} diff --git a/test/com/esotericsoftware/kryo/serializers/JavaSerializerTest.java b/test/com/esotericsoftware/kryo/serializers/JavaSerializerTest.java index 078672299..5f20f16f8 100644 --- a/test/com/esotericsoftware/kryo/serializers/JavaSerializerTest.java +++ b/test/com/esotericsoftware/kryo/serializers/JavaSerializerTest.java @@ -19,14 +19,23 @@ package com.esotericsoftware.kryo.serializers; +import com.esotericsoftware.kryo.KryoException; import com.esotericsoftware.kryo.KryoTestCase; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; import java.io.Serializable; +import java.util.List; +import java.util.ArrayList; import java.net.URL; import java.net.URLClassLoader; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + /** @author Nathan Sweet */ class JavaSerializerTest extends KryoTestCase { @Test @@ -55,6 +64,64 @@ void testJavaSerializerFallbackToDefaultClassLoader () { roundTrip(139, test); } + @Test + void testClassFilterRejectsDisallowedClass () { + JavaSerializer serializer = new JavaSerializer(); + serializer.setClassFilter(name -> !name.equals(TestClass.class.getName())); + kryo.register(TestClass.class, serializer); + + TestClass test = new TestClass(); + test.stringField = "fubar"; + test.intField = 54321; + + Output output = new Output(1024, -1); + kryo.writeObject(output, test); + + Input input = new Input(output.toBytes()); + assertThrows(KryoException.class, () -> kryo.readObject(input, TestClass.class)); + } + + @Test + void testClassFilterAllowsClass () { + JavaSerializer serializer = new JavaSerializer(); + serializer.setClassFilter(name -> true); + kryo.register(TestClass.class, serializer); + + TestClass test = new TestClass(); + test.stringField = "fubar"; + test.intField = 54321; + + Output output = new Output(1024, -1); + kryo.writeObject(output, test); + + Input input = new Input(output.toBytes()); + assertEquals(test, kryo.readObject(input, TestClass.class)); + } + + @Test + void testClassFilterSeesTheClassName () { + List seen = new ArrayList<>(); + JavaSerializer serializer = new JavaSerializer(); + serializer.setClassFilter(name -> { + seen.add(name); + return true; + }); + kryo.register(TestClass.class, serializer); + + TestClass test = new TestClass(); + test.stringField = "fubar"; + test.intField = 54321; + + Output output = new Output(1024, -1); + kryo.writeObject(output, test); + + Input input = new Input(output.toBytes()); + assertEquals(test, kryo.readObject(input, TestClass.class)); + // The filter runs on the name before the class is resolved, so a caller can refuse a class + // without it being loaded, and can refuse a name that would not resolve at all. + assertTrue(seen.contains(TestClass.class.getName())); + } + public static class TestClass implements Serializable { String stringField; int intField;