INT-4290: JacksonJsonUtils: Add Trusted Packages
JIRA: https://jira.spring.io/browse/INT-4290 See CVE-2017-4995 To disallow deserialization of unknown classes, the `JacksonJsonUtils#messagingAwareMapper()` can now be supplied with the `trustedPackages`. The default list is: ``` java.util java.lang org.springframework.messaging.support org.springframework.integration.support org.springframework.integration.message org.springframework.integration.store ``` Can be configured with `*` (asterisk) with meaning trust all **Cherry-pick to 4.3.x** Polishing according PR comments Conflicts: spring-integration-core/src/main/java/org/springframework/integration/store/AbstractKeyValueMessageStore.java Resolved.
This commit is contained in:
committed by
Gary Russell
parent
de00e72e5e
commit
cac498cac7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2017 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.
|
||||
@@ -36,6 +36,7 @@ import org.springframework.util.Assert;
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
public abstract class AbstractKeyValueMessageStore extends AbstractMessageGroupStore implements MessageStore {
|
||||
@@ -60,10 +61,14 @@ public abstract class AbstractKeyValueMessageStore extends AbstractMessageGroupS
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Message<T> addMessage(Message<T> message) {
|
||||
doAddMessage(message);
|
||||
return (Message<T>) getMessage(message.getHeaders().getId());
|
||||
}
|
||||
|
||||
protected void doAddMessage(Message<?> message) {
|
||||
Assert.notNull(message, "'message' must not be null");
|
||||
UUID messageId = message.getHeaders().getId();
|
||||
doStore(MESSAGE_KEY_PREFIX + messageId, message);
|
||||
return (Message<T>) getRawMessage(messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -133,7 +138,7 @@ public abstract class AbstractKeyValueMessageStore extends AbstractMessageGroupS
|
||||
for (Message<?> message : messages) {
|
||||
// enrich Message with additional headers and add it to MS
|
||||
Message<?> enrichedMessage = enrichMessage(message);
|
||||
addMessage(enrichedMessage);
|
||||
doAddMessage(message);
|
||||
if (metadata != null) {
|
||||
metadata.add(enrichedMessage.getHeaders().getId());
|
||||
}
|
||||
|
||||
@@ -16,15 +16,28 @@
|
||||
|
||||
package org.springframework.integration.support.json;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.integration.message.AdviceMessage;
|
||||
import org.springframework.integration.support.MutableMessage;
|
||||
import org.springframework.messaging.support.ErrorMessage;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.databind.DatabindContext;
|
||||
import com.fasterxml.jackson.databind.DeserializationConfig;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
import com.fasterxml.jackson.databind.MapperFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.cfg.MapperConfig;
|
||||
import com.fasterxml.jackson.databind.jsontype.NamedType;
|
||||
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
|
||||
/**
|
||||
@@ -64,16 +77,18 @@ public final class JacksonJsonUtils {
|
||||
* Return an {@link com.fasterxml.jackson.databind.ObjectMapper} if available,
|
||||
* supplied with Message specific serializers and deserializers.
|
||||
* Also configured to store typo info in the {@code @class} property.
|
||||
* @param trustedPackages the trusted Java packages for deserialization.
|
||||
* @return the mapper.
|
||||
* @throws IllegalStateException if an implementation is not available.
|
||||
* @since 4.3.10
|
||||
*/
|
||||
public static com.fasterxml.jackson.databind.ObjectMapper messagingAwareMapper() {
|
||||
public static com.fasterxml.jackson.databind.ObjectMapper messagingAwareMapper(String... trustedPackages) {
|
||||
if (JacksonPresent.isJackson2Present()) {
|
||||
ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
|
||||
|
||||
mapper.setDefaultTyping(new WhitelistTypeResolverBuilder(trustedPackages));
|
||||
|
||||
GenericMessageJacksonDeserializer genericMessageDeserializer = new GenericMessageJacksonDeserializer();
|
||||
genericMessageDeserializer.setMapper(mapper);
|
||||
@@ -101,4 +116,144 @@ public final class JacksonJsonUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of {@link ObjectMapper.DefaultTypeResolverBuilder}
|
||||
* that wraps a default {@link TypeIdResolver} to the {@link WhitelistTypeIdResolver}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 4.3.11
|
||||
*/
|
||||
private static final class WhitelistTypeResolverBuilder extends ObjectMapper.DefaultTypeResolverBuilder {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String[] trustedPackages;
|
||||
|
||||
WhitelistTypeResolverBuilder(String... trustedPackages) {
|
||||
super(ObjectMapper.DefaultTyping.NON_FINAL);
|
||||
this.trustedPackages = trustedPackages;
|
||||
|
||||
init(JsonTypeInfo.Id.CLASS, null)
|
||||
.inclusion(JsonTypeInfo.As.PROPERTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TypeIdResolver idResolver(MapperConfig<?> config, JavaType baseType, Collection<NamedType> subtypes,
|
||||
boolean forSer, boolean forDeser) {
|
||||
TypeIdResolver delegate = super.idResolver(config, baseType, subtypes, forSer, forDeser);
|
||||
return new WhitelistTypeIdResolver(delegate, this.trustedPackages);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link TypeIdResolver} that delegates to an existing implementation
|
||||
* and throws an IllegalStateException if the class being looked up is not whitelisted,
|
||||
* does not provide an explicit mixin mappings.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 4.3.11
|
||||
*/
|
||||
private static final class WhitelistTypeIdResolver implements TypeIdResolver {
|
||||
|
||||
private static final List<String> TRUSTED_PACKAGES =
|
||||
Arrays.asList(
|
||||
"java.util",
|
||||
"java.lang",
|
||||
"org.springframework.messaging.support",
|
||||
"org.springframework.integration.support",
|
||||
"org.springframework.integration.message",
|
||||
"org.springframework.integration.store"
|
||||
);
|
||||
|
||||
private final TypeIdResolver delegate;
|
||||
|
||||
private final Set<String> trustedPackages = new LinkedHashSet<String>(TRUSTED_PACKAGES);
|
||||
|
||||
WhitelistTypeIdResolver(TypeIdResolver delegate, String... trustedPackages) {
|
||||
this.delegate = delegate;
|
||||
if (trustedPackages != null) {
|
||||
for (String whiteListClass : trustedPackages) {
|
||||
if ("*".equals(whiteListClass)) {
|
||||
this.trustedPackages.clear();
|
||||
break;
|
||||
}
|
||||
else {
|
||||
this.trustedPackages.add(whiteListClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(JavaType baseType) {
|
||||
this.delegate.init(baseType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String idFromValue(Object value) {
|
||||
return this.delegate.idFromValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String idFromValueAndType(Object value, Class<?> suggestedType) {
|
||||
return this.delegate.idFromValueAndType(value, suggestedType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String idFromBaseType() {
|
||||
return this.delegate.idFromBaseType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType typeFromId(DatabindContext context, String id) throws IOException {
|
||||
DeserializationConfig config = (DeserializationConfig) context.getConfig();
|
||||
JavaType result = this.delegate.typeFromId(context, id);
|
||||
|
||||
String packageName = result.getRawClass().getPackage().getName();
|
||||
if (isTrustedPackage(packageName)) {
|
||||
return this.delegate.typeFromId(context, id);
|
||||
}
|
||||
|
||||
boolean isExplicitMixin = config.findMixInClassFor(result.getRawClass()) != null;
|
||||
if (isExplicitMixin) {
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("The class with " + id + " and name of " +
|
||||
"" + result.getRawClass().getName() + " is not in the trusted packages: " +
|
||||
"" + this.trustedPackages + ". " +
|
||||
"If you believe this class is safe to deserialize, please provide its name or an explicit Mixin. " +
|
||||
"If the serialization is only done by a trusted source, you can also enable default typing.");
|
||||
}
|
||||
|
||||
private boolean isTrustedPackage(String packageName) {
|
||||
if (!this.trustedPackages.isEmpty()) {
|
||||
for (String trustedPackage : this.trustedPackages) {
|
||||
if (packageName.equals(trustedPackage) || packageName.startsWith(trustedPackage + ".")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescForKnownTypeIds() {
|
||||
return this.delegate.getDescForKnownTypeIds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonTypeInfo.Id getMechanism() {
|
||||
return this.delegate.getMechanism();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.integration.redis.store;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
@@ -23,11 +24,13 @@ import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -460,6 +463,83 @@ public class RedisMessageGroupStoreTests extends RedisAvailableTests {
|
||||
assertThat(errorMessageResult, instanceOf(ErrorMessage.class));
|
||||
assertEquals(errorMessage.getPayload().getMessage(),
|
||||
((ErrorMessage) errorMessageResult).getPayload().getMessage());
|
||||
|
||||
Message<Foo> fooMessage = new GenericMessage<>(new Foo("foo"));
|
||||
try {
|
||||
store.addMessageToGroup(1, fooMessage)
|
||||
.getMessages()
|
||||
.iterator()
|
||||
.next();
|
||||
fail("SerializationException expected");
|
||||
}
|
||||
catch (Exception e) {
|
||||
assertThat(e.getCause().getCause(), instanceOf(IllegalArgumentException.class));
|
||||
assertThat(e.getMessage(),
|
||||
containsString("The class with " +
|
||||
"org.springframework.integration.redis.store.RedisMessageGroupStoreTests$Foo and name of " +
|
||||
"org.springframework.integration.redis.store.RedisMessageGroupStoreTests$Foo " +
|
||||
"is not in the trusted packages:"));
|
||||
}
|
||||
|
||||
mapper = JacksonJsonUtils.messagingAwareMapper(getClass().getPackage().getName());
|
||||
|
||||
serializer = new GenericJackson2JsonRedisSerializer(mapper);
|
||||
store.setValueSerializer(serializer);
|
||||
|
||||
store.removeMessageGroup(1);
|
||||
messageGroup = store.addMessageToGroup(1, fooMessage);
|
||||
assertEquals(1, messageGroup.size());
|
||||
assertEquals(fooMessage, messageGroup.getMessages().iterator().next());
|
||||
|
||||
mapper = JacksonJsonUtils.messagingAwareMapper("*");
|
||||
|
||||
serializer = new GenericJackson2JsonRedisSerializer(mapper);
|
||||
store.setValueSerializer(serializer);
|
||||
|
||||
store.removeMessageGroup(1);
|
||||
messageGroup = store.addMessageToGroup(1, fooMessage);
|
||||
assertEquals(1, messageGroup.size());
|
||||
assertEquals(fooMessage, messageGroup.getMessages().iterator().next());
|
||||
}
|
||||
|
||||
private static class Foo {
|
||||
|
||||
private String foo;
|
||||
|
||||
Foo() {
|
||||
}
|
||||
|
||||
Foo(String foo) {
|
||||
this.foo = foo;
|
||||
}
|
||||
|
||||
public String getFoo() {
|
||||
return this.foo;
|
||||
}
|
||||
|
||||
public void setFoo(String foo) {
|
||||
this.foo = foo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Foo foo1 = (Foo) o;
|
||||
|
||||
return this.foo != null ? this.foo.equals(foo1.foo) : foo1.foo == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(this.foo);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user