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:
Artem Bilan
2017-06-08 11:40:16 -04:00
committed by Gary Russell
parent de00e72e5e
commit cac498cac7
3 changed files with 245 additions and 5 deletions

View File

@@ -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());
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}