diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/Jackson2ExecutionContextStringSerializer.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/Jackson2ExecutionContextStringSerializer.java index f20136f42..1c8858c82 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/Jackson2ExecutionContextStringSerializer.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/Jackson2ExecutionContextStringSerializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2019 the original author or authors. + * Copyright 2008-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,28 +18,80 @@ package org.springframework.batch.core.repository.dao; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; +import com.fasterxml.jackson.annotation.JacksonAnnotation; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DatabindContext; +import com.fasterxml.jackson.databind.DeserializationConfig; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; - +import com.fasterxml.jackson.databind.cfg.MapperConfig; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; +import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; +import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; import com.fasterxml.jackson.databind.module.SimpleModule; + import org.springframework.batch.core.JobParameter; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.repository.ExecutionContextSerializer; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.Assert; /** - * Implementation that uses Jackson2 to provide (de)serialization. + * Implementation that uses Jackson2 to provide (de)serialization. + * + * By default, this implementation trusts a limited set of classes to be + * deserialized from the execution context. If a class is not trusted by default + * and is safe to deserialize, you can provide an explicit mapping using Jackson + * annotations, as shown in the following example: + * + *
+ *     @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+ *     public class MyTrustedType implements Serializable {
+ *        
+ *     }
+ * 
+ * + * It is also possible to provide a custom {@link ObjectMapper} with a mixin for + * the trusted type: + * + *
+ *     ObjectMapper objectMapper = new ObjectMapper();
+ *     objectMapper.addMixIn(MyTrustedType.class, Object.class);
+ *     Jackson2ExecutionContextStringSerializer serializer = new Jackson2ExecutionContextStringSerializer();
+ *     serializer.setObjectMapper(objectMapper);
+ *     // register serializer in JobRepositoryFactoryBean
+ * 
+ * + * If the (de)serialization is only done by a trusted source, you can also enable + * default typing: + * + *
+ *     PolymorphicTypeValidator polymorphicTypeValidator = .. // configure your trusted PolymorphicTypeValidator
+ *     ObjectMapper objectMapper = new ObjectMapper();
+ *     objectMapper.activateDefaultTyping(polymorphicTypeValidator); 
+ *     Jackson2ExecutionContextStringSerializer serializer = new Jackson2ExecutionContextStringSerializer();
+ *     serializer.setObjectMapper(objectMapper);
+ *     // register serializer in JobRepositoryFactoryBean
+ * 
* * @author Marten Deinum * @author Mahmoud Ben Hassine @@ -55,7 +107,8 @@ public class Jackson2ExecutionContextStringSerializer implements ExecutionContex this.objectMapper = new ObjectMapper(); this.objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false); this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true); - this.objectMapper.enableDefaultTyping(); + this.objectMapper.configure(MapperFeature.BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES, true); + this.objectMapper.setDefaultTyping(createTrustedDefaultTyping()); this.objectMapper.registerModule(new JobParametersModule()); } @@ -141,4 +194,158 @@ public class Jackson2ExecutionContextStringSerializer implements ExecutionContex } + /** + * Creates a TypeResolverBuilder that checks if a type is trusted. + * @return a TypeResolverBuilder that checks if a type is trusted. + */ + private static TypeResolverBuilder createTrustedDefaultTyping() { + TypeResolverBuilder result = new TrustedTypeResolverBuilder(ObjectMapper.DefaultTyping.NON_FINAL); + result = result.init(JsonTypeInfo.Id.CLASS, null); + result = result.inclusion(JsonTypeInfo.As.PROPERTY); + return result; + } + + /** + * An implementation of {@link ObjectMapper.DefaultTypeResolverBuilder} + * that inserts an {@code allow all} {@link PolymorphicTypeValidator} + * and overrides the {@code TypeIdResolver} + * @author Rob Winch + */ + static class TrustedTypeResolverBuilder extends ObjectMapper.DefaultTypeResolverBuilder { + + TrustedTypeResolverBuilder(ObjectMapper.DefaultTyping defaultTyping) { + super( + defaultTyping, + //we do explicit validation in the TypeIdResolver + BasicPolymorphicTypeValidator.builder() + .allowIfSubType(Object.class) + .build() + ); + } + + @Override + protected TypeIdResolver idResolver(MapperConfig config, + JavaType baseType, + PolymorphicTypeValidator subtypeValidator, + Collection subtypes, boolean forSer, boolean forDeser) { + TypeIdResolver result = super.idResolver(config, baseType, subtypeValidator, subtypes, forSer, forDeser); + return new TrustedTypeIdResolver(result); + } + } + + /** + * A {@link TypeIdResolver} that delegates to an existing implementation and throws an IllegalStateException if the + * class being looked up is not trusted, does not provide an explicit mixin, and is not annotated with Jackson + * mappings. + */ + static class TrustedTypeIdResolver implements TypeIdResolver { + private static final Set TRUSTED_CLASS_NAMES = Collections.unmodifiableSet(new HashSet(Arrays.asList( + "java.util.ArrayList", + "java.util.LinkedList", + "java.util.Collections$EmptyList", + "java.util.Collections$EmptyMap", + "java.util.Collections$EmptySet", + "java.util.Collections$UnmodifiableRandomAccessList", + "java.util.Collections$UnmodifiableList", + "java.util.Collections$UnmodifiableMap", + "java.util.Collections$UnmodifiableSet", + "java.util.Collections$SingletonList", + "java.util.Collections$SingletonMap", + "java.util.Collections$SingletonSet", + "java.util.Date", + "java.time.Instant", + "java.time.Duration", + "java.time.LocalDate", + "java.time.LocalTime", + "java.time.LocalDateTime", + "java.net.URL", + "java.util.TreeMap", + "java.util.HashMap", + "java.util.LinkedHashMap", + "java.util.TreeSet", + "java.util.HashSet", + "java.util.LinkedHashSet", + "java.lang.Boolean", + "java.lang.Byte", + "java.lang.Short", + "java.lang.Integer", + "java.lang.Long", + "java.lang.Double", + "java.lang.Float", + "java.math.BigDecimal", + "java.math.BigInteger", + "java.lang.String", + "java.lang.Character", + "java.lang.CharSequence", + "java.util.Properties", + "[Ljava.util.Properties;", + "org.springframework.batch.core.JobParameter", + "org.springframework.batch.core.JobParameters", + "org.springframework.batch.core.jsr.partition.JsrPartitionHandler$PartitionPlanState" + ))); + + private final TypeIdResolver delegate; + + TrustedTypeIdResolver(TypeIdResolver delegate) { + this.delegate = delegate; + } + + @Override + public void init(JavaType baseType) { + delegate.init(baseType); + } + + @Override + public String idFromValue(Object value) { + return delegate.idFromValue(value); + } + + @Override + public String idFromValueAndType(Object value, Class suggestedType) { + return delegate.idFromValueAndType(value, suggestedType); + } + + @Override + public String idFromBaseType() { + return delegate.idFromBaseType(); + } + + @Override + public JavaType typeFromId(DatabindContext context, String id) throws IOException { + DeserializationConfig config = (DeserializationConfig) context.getConfig(); + JavaType result = delegate.typeFromId(context, id); + String className = result.getRawClass().getName(); + if (isTrusted(className)) { + return result; + } + boolean isExplicitMixin = config.findMixInClassFor(result.getRawClass()) != null; + if (isExplicitMixin) { + return result; + } + Class rawClass = result.getRawClass(); + JacksonAnnotation jacksonAnnotation = AnnotationUtils.findAnnotation(rawClass, JacksonAnnotation.class); + if (jacksonAnnotation != null) { + return result; + } + throw new IllegalArgumentException("The class with " + id + " and name of " + className + " is not trusted. " + + "If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or a custom ObjectMapper. " + + "If the serialization is only done by a trusted source, you can also enable default typing."); + } + + private boolean isTrusted(String id) { + return TRUSTED_CLASS_NAMES.contains(id); + } + + @Override + public String getDescForKnownTypeIds() { + return delegate.getDescForKnownTypeIds(); + } + + @Override + public JsonTypeInfo.Id getMechanism() { + return delegate.getMechanism(); + } + + } + } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/dao/AbstractExecutionContextSerializerTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/dao/AbstractExecutionContextSerializerTests.java index 2462ed4ef..5d2a8b61b 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/dao/AbstractExecutionContextSerializerTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/dao/AbstractExecutionContextSerializerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ package org.springframework.batch.core.repository.dao; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.junit.Test; import org.springframework.batch.core.JobParameter; import org.springframework.batch.core.JobParameters; @@ -175,6 +176,7 @@ public abstract class AbstractExecutionContextSerializerTests { protected abstract ExecutionContextSerializer getSerializer(); + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) public static class ComplexObject implements Serializable { private static final long serialVersionUID = 1L; private String name; diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/dao/Jackson2ExecutionContextStringSerializerTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/dao/Jackson2ExecutionContextStringSerializerTests.java index 0987a4203..9870f1180 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/dao/Jackson2ExecutionContextStringSerializerTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/dao/Jackson2ExecutionContextStringSerializerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2016 the original author or authors. + * Copyright 2008-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,24 @@ */ package org.springframework.batch.core.repository.dao; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.junit.Before; +import org.junit.Test; + import org.springframework.batch.core.repository.ExecutionContextSerializer; +import static org.junit.Assert.fail; + /** * @author Marten Deinum + * @author Michael Minella */ public class Jackson2ExecutionContextStringSerializerTests extends AbstractExecutionContextSerializerTests { @@ -32,8 +45,99 @@ public class Jackson2ExecutionContextStringSerializerTests extends AbstractExecu serializer = serializerDeserializer; } + @Test + public void mappedTypeTest() throws IOException { + + Person person = new Person(); + person.age = 28; + person.name = "Bob"; + person.phone = new DomesticNumber(); + person.phone.areaCode = 555; + person.phone.local = 1234567; + + Jackson2ExecutionContextStringSerializer j = new Jackson2ExecutionContextStringSerializer(); + + Map context = new HashMap<>(1); + context.put("person", person); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + j.serialize(context, os); + + InputStream in = new ByteArrayInputStream(os.toByteArray()); + + try { + j.deserialize(in); + } + catch (Exception e) { + fail(String.format("An exception was thrown but should not have been: %s", e.getMessage())); + } + } + @Override protected ExecutionContextSerializer getSerializer() { return this.serializer; } + + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) + public static class Person { + public String name; + public int age; + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) + public PhoneNumber phone; + } + + public static abstract class PhoneNumber { + public int areaCode, local; + } + + public static class InternationalNumber extends PhoneNumber { + public int countryCode; + } + + public static class DomesticNumber extends PhoneNumber{} + + @Test + public void unmappedTypeTest() throws IOException { + + UnmappedPerson person = new UnmappedPerson(); + person.age = 28; + person.name = "Bob"; + person.phone = new UnmappedDomesticNumber(); + person.phone.areaCode = 555; + person.phone.local = 1234567; + + Jackson2ExecutionContextStringSerializer j = new Jackson2ExecutionContextStringSerializer(); + + Map context = new HashMap<>(1); + context.put("person", person); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + j.serialize(context, os); + + InputStream in = new ByteArrayInputStream(os.toByteArray()); + + try { + j.deserialize(in); + fail("An exception should have been thrown but wasn't"); + } + catch (Exception e) { + return; + } + } + + public static class UnmappedPerson { + public String name; + public int age; + public UnmappedPhoneNumber phone; + } + + public static abstract class UnmappedPhoneNumber { + public int areaCode, local; + } + + public static class UnmappedInternationalNumber extends UnmappedPhoneNumber { + public int countryCode; + } + + public static class UnmappedDomesticNumber extends UnmappedPhoneNumber{} }