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 extends TypeResolverBuilder> createTrustedDefaultTyping() {
+ TypeResolverBuilder extends 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{}
}