From f9721b537d17cd9b1a003808c1044d9919a88fb9 Mon Sep 17 00:00:00 2001 From: Nexory Date: Mon, 3 Aug 2026 02:55:22 +0200 Subject: [PATCH 1/2] Add an optional JEP-290 ObjectInputFilter to JdkSerializer JdkSerializer.deserialize reads Java-serialized bytes back from a process-external store (it is the default cache and session value serializer used by micronaut-redis) and previously installed no java.io.ObjectInputFilter, so any class on the classpath could be deserialized from that input. Add an opt-in filter, configurable via a new JdkSerializer(ConversionService, ObjectInputFilter) constructor and an optional micronaut.serializer.jdk.serial-filter system property, and apply it in createObjectInput. The default is unchanged (no filter) so existing deployments are unaffected. resolveClass is left untouched. --- .../core/serialize/JdkSerializer.java | 36 ++++++++++++++++++- .../core/serialize/JdkSerializerSpec.groovy | 30 ++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) 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..c88831793ed 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,14 @@ public JdkSerializer() { this(ConversionService.SHARED); } + private static @Nullable ObjectInputFilter resolveDefaultFilter() { + String pattern = System.getProperty(SERIAL_FILTER_PROPERTY); + if (pattern == null || pattern.isBlank()) { + return null; + } + return ObjectInputFilter.Config.createFilter(pattern); + } + @Override public void serialize(@Nullable Object object, OutputStream outputStream) throws SerializationException { try { @@ -125,7 +155,7 @@ private ObjectOutputStream createObjectOutput(OutputStream outputStream) throws * @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 +165,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..0676413bcd2 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,32 @@ 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" + } + static class Foo implements Serializable { String name } From b758137027368667a0b22fda8e11b0a20e6eab95 Mon Sep 17 00:00:00 2001 From: Nexory Date: Tue, 4 Aug 2026 14:06:34 +0200 Subject: [PATCH 2/2] Address review feedback on JdkSerializer serial filter - Wrap the IllegalArgumentException from ObjectInputFilter.Config.createFilter so a misconfigured micronaut.serializer.jdk.serial-filter names the property and the offending value. - Fix the createObjectInput javadoc return type (ObjectInputStream). --- .../micronaut/core/serialize/JdkSerializer.java | 8 ++++++-- .../core/serialize/JdkSerializerSpec.groovy | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) 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 c88831793ed..5cd1d25ff66 100644 --- a/core/src/main/java/io/micronaut/core/serialize/JdkSerializer.java +++ b/core/src/main/java/io/micronaut/core/serialize/JdkSerializer.java @@ -80,7 +80,11 @@ public JdkSerializer() { if (pattern == null || pattern.isBlank()) { return null; } - return ObjectInputFilter.Config.createFilter(pattern); + try { + return ObjectInputFilter.Config.createFilter(pattern); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid " + SERIAL_FILTER_PROPERTY + " value [" + pattern + "]: " + e.getMessage(), e); + } } @Override @@ -151,7 +155,7 @@ 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 { 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 0676413bcd2..c9c3900f450 100644 --- a/core/src/test/groovy/io/micronaut/core/serialize/JdkSerializerSpec.groovy +++ b/core/src/test/groovy/io/micronaut/core/serialize/JdkSerializerSpec.groovy @@ -71,6 +71,21 @@ class JdkSerializerSpec extends Specification { 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 }