diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/JacksonJsonMessageConverter.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/JacksonJsonMessageConverter.java index a8a70006672..bb9f5f1977a 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/converter/JacksonJsonMessageConverter.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/JacksonJsonMessageConverter.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -48,6 +49,8 @@ import org.springframework.util.ClassUtils; * {@link #setTargetType targetType} is set to {@link MessageType#TEXT}. * Converts from a {@link TextMessage} or {@link BytesMessage} to an object. * + *

For untrusted environments, use {@link #setTrustedPackages(String...)}. + * * @author Sebastien Deleuze * @since 7.0 */ @@ -73,6 +76,8 @@ public class JacksonJsonMessageConverter implements SmartMessageConverter, BeanC private final Map, String> classIdMappings = new HashMap<>(); + private String @Nullable [] trustedPackages; + private @Nullable ClassLoader beanClassLoader; @@ -80,6 +85,7 @@ public class JacksonJsonMessageConverter implements SmartMessageConverter, BeanC * Construct a new instance with a {@link JsonMapper} customized with the * {@link tools.jackson.databind.JacksonModule}s found by * {@link MapperBuilder#findModules(ClassLoader)}. + * @see #setTrustedPackages(String...) */ public JacksonJsonMessageConverter() { this(JsonMapper.builder()); @@ -89,6 +95,8 @@ public class JacksonJsonMessageConverter implements SmartMessageConverter, BeanC * Construct a new instance with the provided {@link JsonMapper.Builder} * customized with the {@link tools.jackson.databind.JacksonModule}s found * by {@link MapperBuilder#findModules(ClassLoader)}. + * @param builder the mapper builder to use + * @see #setTrustedPackages(String...) * @see JsonMapper#builder() */ public JacksonJsonMessageConverter(JsonMapper.Builder builder) { @@ -98,6 +106,8 @@ public class JacksonJsonMessageConverter implements SmartMessageConverter, BeanC /** * Construct a new instance with the provided {@link JsonMapper}. + * @param mapper the mapper to use + * @see #setTrustedPackages(String...) * @see JsonMapper#builder() */ public JacksonJsonMessageConverter(JsonMapper mapper) { @@ -105,6 +115,15 @@ public class JacksonJsonMessageConverter implements SmartMessageConverter, BeanC this.mapper = mapper; } + /** + * Specify the trusted Java packages for deserialization. + * @param trustedPackages the trusted Java packages for deserialization + * @since 7.0.8 + */ + public void setTrustedPackages(String... trustedPackages) { + this.trustedPackages = trustedPackages.clone(); + } + /** * Specify whether {@link #toMessage(Object, Session)} should marshal to a * {@link BytesMessage} or a {@link TextMessage}. @@ -168,6 +187,23 @@ public class JacksonJsonMessageConverter implements SmartMessageConverter, BeanC }); } + private boolean isTrustedPackage(String requestedType) { + if (this.trustedPackages != null) { + String packageName = ClassUtils.getPackageName(requestedType); + int lastBracketIndex = packageName.lastIndexOf('['); + if (lastBracketIndex != -1 && packageName.length() > lastBracketIndex + 1 && packageName.charAt(lastBracketIndex + 1) == 'L') { + packageName = packageName.substring(lastBracketIndex + 2); + } + for (String trustedPackage : this.trustedPackages) { + if (packageName.equals(trustedPackage)) { + return true; + } + } + return false; + } + return true; + } + @Override public void setBeanClassLoader(ClassLoader classLoader) { this.beanClassLoader = classLoader; @@ -445,6 +481,10 @@ public class JacksonJsonMessageConverter implements SmartMessageConverter, BeanC if (mappedClass != null) { return this.mapper.constructType(mappedClass); } + if (!isTrustedPackage(typeId)) { + throw new MessageConversionException("The class '" + typeId + "' is not in the trusted packages: " + + Arrays.toString(this.trustedPackages)); + } try { Class typeClass = ClassUtils.forName(typeId, this.beanClassLoader); return this.mapper.constructType(typeClass); diff --git a/spring-jms/src/test/java/org/springframework/jms/support/converter/JacksonJsonMessageConverterTests.java b/spring-jms/src/test/java/org/springframework/jms/support/converter/JacksonJsonMessageConverterTests.java index b01f421512e..d67b68003bd 100644 --- a/spring-jms/src/test/java/org/springframework/jms/support/converter/JacksonJsonMessageConverterTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/support/converter/JacksonJsonMessageConverterTests.java @@ -37,6 +37,7 @@ import org.mockito.stubbing.Answer; import org.springframework.core.MethodParameter; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.isA; @@ -131,6 +132,86 @@ class JacksonJsonMessageConverterTests { assertThat(unmarshalled).as("Invalid result").isEqualTo(result); } + @Test + void fromTextMessageUntrusted() throws Exception { + converter = new JacksonJsonMessageConverter(); + converter.setTrustedPackages("java.lang"); + converter.setTypeIdPropertyName("__typeid__"); + TextMessage textMessageMock = mock(); + + String text = "{\"foo\":\"bar\"}"; + given(textMessageMock.getStringProperty("__typeid__")).willReturn(MyBean.class.getName()); + given(textMessageMock.getText()).willReturn(text); + + assertThatExceptionOfType(MessageConversionException.class) + .isThrownBy(() -> converter.fromMessage(textMessageMock)) + .withMessageContaining("is not in the trusted packages"); + } + + @Test + void fromTextMessageTrusted() throws Exception { + converter = new JacksonJsonMessageConverter(); + converter.setTrustedPackages("java.lang", "org.springframework.jms.support.converter"); + converter.setTypeIdPropertyName("__typeid__"); + TextMessage textMessageMock = mock(); + MyBean unmarshalled = new MyBean("bar"); + + String text = "{\"foo\":\"bar\"}"; + given(textMessageMock.getStringProperty("__typeid__")).willReturn(MyBean.class.getName()); + given(textMessageMock.getText()).willReturn(text); + + MyBean result = (MyBean) converter.fromMessage(textMessageMock); + assertThat(unmarshalled).as("Invalid result").isEqualTo(result); + } + + @Test + void fromTextMessageTrustedEmpty() throws Exception { + converter = new JacksonJsonMessageConverter(); + converter.setTrustedPackages(); + converter.setTypeIdPropertyName("__typeid__"); + TextMessage textMessageMock = mock(); + + String text = "{\"foo\":\"bar\"}"; + given(textMessageMock.getStringProperty("__typeid__")).willReturn(MyBean.class.getName()); + given(textMessageMock.getText()).willReturn(text); + + assertThatExceptionOfType(MessageConversionException.class) + .isThrownBy(() -> converter.fromMessage(textMessageMock)) + .withMessageContaining("is not in the trusted packages"); + } + + @Test + void fromTextMessageTrusted1DArray() throws Exception { + converter = new JacksonJsonMessageConverter(); + converter.setTrustedPackages("org.springframework.jms.support.converter"); + converter.setTypeIdPropertyName("__typeid__"); + TextMessage textMessageMock = mock(); + MyBean[] unmarshalled = new MyBean[] { new MyBean("bar") }; + + String text = "[{\"foo\":\"bar\"}]"; + given(textMessageMock.getStringProperty("__typeid__")).willReturn("[L" + MyBean.class.getName() + ";"); + given(textMessageMock.getText()).willReturn(text); + + MyBean[] result = (MyBean[]) converter.fromMessage(textMessageMock); + assertThat(unmarshalled).as("Invalid result").isEqualTo(result); + } + + @Test + void fromTextMessageTrusted2DArray() throws Exception { + converter = new JacksonJsonMessageConverter(); + converter.setTrustedPackages("org.springframework.jms.support.converter"); + converter.setTypeIdPropertyName("__typeid__"); + TextMessage textMessageMock = mock(); + MyBean[][] unmarshalled = new MyBean[][] { { new MyBean("bar") } }; + + String text = "[[{\"foo\":\"bar\"}]]"; + given(textMessageMock.getStringProperty("__typeid__")).willReturn("[[L" + MyBean.class.getName() + ";"); + given(textMessageMock.getText()).willReturn(text); + + MyBean[][] result = (MyBean[][]) converter.fromMessage(textMessageMock); + assertThat(unmarshalled).as("Invalid result").isEqualTo(result); + } + @Test void fromTextMessageWithUnknownProperty() throws Exception { TextMessage textMessageMock = mock();