diff --git a/core/src/main/java/io/micronaut/core/serialize/JdkSerializer.java b/core/src/main/java/io/micronaut/core/serialize/JdkSerializer.java index 4a62cd2dd06..5cd1d25ff66 100644 --- a/core/src/main/java/io/micronaut/core/serialize/JdkSerializer.java +++ b/core/src/main/java/io/micronaut/core/serialize/JdkSerializer.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.io.InputStream; +import java.io.ObjectInputFilter; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.ObjectStreamClass; @@ -37,13 +38,34 @@ */ public final class JdkSerializer implements ObjectSerializer { + /** + * System property that, when set to a non-blank {@link ObjectInputFilter} pattern (as accepted by + * {@link ObjectInputFilter.Config#createFilter(String)}), installs a JEP-290 deserialization filter + * on every stream created by this serializer. Unset by default, which preserves the previous + * (unfiltered) behaviour. + * + * @since 5.2.0 + */ + public static final String SERIAL_FILTER_PROPERTY = "micronaut.serializer.jdk.serial-filter"; + private final ConversionService conversionService; + private final @Nullable ObjectInputFilter objectInputFilter; /** * @param conversionService The conversion service */ public JdkSerializer(ConversionService conversionService) { + this(conversionService, resolveDefaultFilter()); + } + + /** + * @param conversionService The conversion service + * @param objectInputFilter The {@link ObjectInputFilter} to apply when deserializing, or {@code null} to apply none + * @since 5.2.0 + */ + public JdkSerializer(ConversionService conversionService, @Nullable ObjectInputFilter objectInputFilter) { this.conversionService = conversionService; + this.objectInputFilter = objectInputFilter; } /** @@ -53,6 +75,18 @@ public JdkSerializer() { this(ConversionService.SHARED); } + private static @Nullable ObjectInputFilter resolveDefaultFilter() { + String pattern = System.getProperty(SERIAL_FILTER_PROPERTY); + if (pattern == null || pattern.isBlank()) { + return null; + } + try { + return ObjectInputFilter.Config.createFilter(pattern); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid " + SERIAL_FILTER_PROPERTY + " value [" + pattern + "]: " + e.getMessage(), e); + } + } + @Override public void serialize(@Nullable Object object, OutputStream outputStream) throws SerializationException { try { @@ -121,11 +155,11 @@ private ObjectOutputStream createObjectOutput(OutputStream outputStream) throws /** * @param inputStream The input stream * @param requiredType The required type - * @return A {@link ObjectOutputStream} + * @return A {@link ObjectInputStream} * @throws IOException if there is an error */ private ObjectInputStream createObjectInput(InputStream inputStream, Class requiredType) throws IOException { - return new ObjectInputStream(inputStream) { + ObjectInputStream objectInput = new ObjectInputStream(inputStream) { @Override protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { Optional> aClass = ClassUtils.forName(desc.getName(), requiredType.getClassLoader()); @@ -135,5 +169,9 @@ protected Class resolveClass(ObjectStreamClass desc) throws IOException, Clas return super.resolveClass(desc); } }; + if (objectInputFilter != null) { + objectInput.setObjectInputFilter(objectInputFilter); + } + return objectInput; } } diff --git a/core/src/test/groovy/io/micronaut/core/serialize/JdkSerializerSpec.groovy b/core/src/test/groovy/io/micronaut/core/serialize/JdkSerializerSpec.groovy index ee4e3c6baf3..c9c3900f450 100644 --- a/core/src/test/groovy/io/micronaut/core/serialize/JdkSerializerSpec.groovy +++ b/core/src/test/groovy/io/micronaut/core/serialize/JdkSerializerSpec.groovy @@ -15,8 +15,12 @@ */ package io.micronaut.core.serialize +import io.micronaut.core.convert.ConversionService +import io.micronaut.core.serialize.exceptions.SerializationException import spock.lang.Specification +import java.io.ObjectInputFilter + /** * @author Graeme Rocher * @since 1.0 @@ -41,6 +45,47 @@ class JdkSerializerSpec extends Specification { !foo.isPresent() } + void 'test deserialization is rejected when an ObjectInputFilter disallows the class'() { + given: + ObjectInputFilter filter = ObjectInputFilter.Config.createFilter('java.lang.*;java.util.*;!*') + def serializer = new JdkSerializer(ConversionService.SHARED, filter) + def bytes = serializer.serialize(new Foo(name: "test")).get() + + when: + serializer.deserialize(bytes, Foo) + + then: + thrown(SerializationException) + } + + void 'test deserialization succeeds when an ObjectInputFilter allows the required type'() { + given: + ObjectInputFilter filter = ObjectInputFilter.Config.createFilter('io.micronaut.core.serialize.JdkSerializerSpec$Foo;java.lang.*;java.util.*;!*') + def serializer = new JdkSerializer(ConversionService.SHARED, filter) + def bytes = serializer.serialize(new Foo(name: "test")).get() + + when: + Foo foo = serializer.deserialize(bytes, Foo).get() + + then: + foo.name == "test" + } + + void 'test an invalid serial-filter system property fails with a message naming the property'() { + given: + System.setProperty(JdkSerializer.SERIAL_FILTER_PROPERTY, 'maxdepth=notanumber') + + when: + new JdkSerializer(ConversionService.SHARED) + + then: + def e = thrown(IllegalArgumentException) + e.message.contains(JdkSerializer.SERIAL_FILTER_PROPERTY) + + cleanup: + System.clearProperty(JdkSerializer.SERIAL_FILTER_PROPERTY) + } + static class Foo implements Serializable { String name }