From cac498cac7c3009fc653ed36bb9622cbb23aa8ef Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 8 Jun 2017 11:40:16 -0400 Subject: [PATCH] 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. --- .../store/AbstractKeyValueMessageStore.java | 11 +- .../support/json/JacksonJsonUtils.java | 159 +++++++++++++++++- .../store/RedisMessageGroupStoreTests.java | 80 +++++++++ 3 files changed, 245 insertions(+), 5 deletions(-) diff --git a/spring-integration-core/src/main/java/org/springframework/integration/store/AbstractKeyValueMessageStore.java b/spring-integration-core/src/main/java/org/springframework/integration/store/AbstractKeyValueMessageStore.java index ac4fb4a9b6..911a0320ee 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/store/AbstractKeyValueMessageStore.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/store/AbstractKeyValueMessageStore.java @@ -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 Message addMessage(Message message) { + doAddMessage(message); + return (Message) 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) 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()); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonJsonUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonJsonUtils.java index 59524591fa..dc73d3ebbf 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonJsonUtils.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonJsonUtils.java @@ -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 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 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 trustedPackages = new LinkedHashSet(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(); + } + + } + } diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/RedisMessageGroupStoreTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/RedisMessageGroupStoreTests.java index 794a38ef43..f88bd242c1 100644 --- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/RedisMessageGroupStoreTests.java +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/RedisMessageGroupStoreTests.java @@ -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 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); + } + } }