Add trusted packages to MappingJackson2MessageConverter

This commit introduces trusted packages, specified via the related
setter for untrusted use cases. It allows explicit configuration
of which Java packages are allowed to be deserialized.

Closes gh-36792
This commit is contained in:
Sébastien Deleuze
2026-06-08 15:13:12 +02:00
committed by Brian Clozel
parent ce90e4b11f
commit ee4e79002b
2 changed files with 120 additions and 0 deletions
@@ -21,6 +21,7 @@ import java.io.IOException;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -49,6 +50,8 @@ import org.springframework.util.ClassUtils;
* {@link #setTargetType targetType} is set to {@link MessageType#TEXT}. * {@link #setTargetType targetType} is set to {@link MessageType#TEXT}.
* Converts from a {@link TextMessage} or {@link BytesMessage} to an object. * Converts from a {@link TextMessage} or {@link BytesMessage} to an object.
* *
* <p>For untrusted environments, use {@link #setTrustedPackages(String...)}.
*
* <p>It customizes Jackson's default properties with the following ones: * <p>It customizes Jackson's default properties with the following ones:
* <ul> * <ul>
* <li>{@link MapperFeature#DEFAULT_VIEW_INCLUSION} is disabled</li> * <li>{@link MapperFeature#DEFAULT_VIEW_INCLUSION} is disabled</li>
@@ -59,6 +62,7 @@ import org.springframework.util.ClassUtils;
* @author Dave Syer * @author Dave Syer
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Sebastien Deleuze
* @since 3.1.4 * @since 3.1.4
*/ */
public class MappingJackson2MessageConverter implements SmartMessageConverter, BeanClassLoaderAware { public class MappingJackson2MessageConverter implements SmartMessageConverter, BeanClassLoaderAware {
@@ -86,12 +90,16 @@ public class MappingJackson2MessageConverter implements SmartMessageConverter, B
private final Map<Class<?>, String> classIdMappings = new HashMap<>(); private final Map<Class<?>, String> classIdMappings = new HashMap<>();
@Nullable
private String[] trustedPackages;
@Nullable @Nullable
private ClassLoader beanClassLoader; private ClassLoader beanClassLoader;
/** /**
* Construct a {@code MappingJackson2MessageConverter} with a default {@link ObjectMapper}. * Construct a {@code MappingJackson2MessageConverter} with a default {@link ObjectMapper}.
* @see #setTrustedPackages(String...)
*/ */
@SuppressWarnings("deprecation") // on Jackson 2.13: configure(MapperFeature, boolean) @SuppressWarnings("deprecation") // on Jackson 2.13: configure(MapperFeature, boolean)
public MappingJackson2MessageConverter() { public MappingJackson2MessageConverter() {
@@ -104,12 +112,21 @@ public class MappingJackson2MessageConverter implements SmartMessageConverter, B
* Construct a {@code MappingJackson2MessageConverter} with a custom {@link ObjectMapper}. * Construct a {@code MappingJackson2MessageConverter} with a custom {@link ObjectMapper}.
* @param objectMapper the {@code ObjectMapper} to use * @param objectMapper the {@code ObjectMapper} to use
* @since 6.1 * @since 6.1
* @see #setTrustedPackages(String...)
*/ */
public MappingJackson2MessageConverter(ObjectMapper objectMapper) { public MappingJackson2MessageConverter(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null"); Assert.notNull(objectMapper, "ObjectMapper must not be null");
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
} }
/**
* Specify the trusted Java packages for deserialization.
* @param trustedPackages the trusted Java packages for deserialization
* @since 6.2.19
*/
public void setTrustedPackages(String... trustedPackages) {
this.trustedPackages = trustedPackages.clone();
}
/** /**
* Set the {@code ObjectMapper} for this converter. * Set the {@code ObjectMapper} for this converter.
@@ -183,6 +200,23 @@ public class MappingJackson2MessageConverter implements SmartMessageConverter, B
}); });
} }
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 @Override
public void setBeanClassLoader(ClassLoader classLoader) { public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = classLoader; this.beanClassLoader = classLoader;
@@ -463,6 +497,10 @@ public class MappingJackson2MessageConverter implements SmartMessageConverter, B
if (mappedClass != null) { if (mappedClass != null) {
return this.objectMapper.constructType(mappedClass); return this.objectMapper.constructType(mappedClass);
} }
if (!isTrustedPackage(typeId)) {
throw new MessageConversionException("The class '" + typeId + "' is not in the trusted packages: " +
Arrays.toString(this.trustedPackages));
}
try { try {
Class<?> typeClass = ClassUtils.forName(typeId, this.beanClassLoader); Class<?> typeClass = ClassUtils.forName(typeId, this.beanClassLoader);
return this.objectMapper.constructType(typeClass); return this.objectMapper.constructType(typeClass);
@@ -37,6 +37,7 @@ import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat; 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.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.isA;
@@ -48,6 +49,7 @@ import static org.mockito.Mockito.verify;
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Dave Syer * @author Dave Syer
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Sebastien Deleuze
*/ */
class MappingJackson2MessageConverterTests { class MappingJackson2MessageConverterTests {
@@ -133,6 +135,86 @@ class MappingJackson2MessageConverterTests {
assertThat(unmarshalled).as("Invalid result").isEqualTo(result); assertThat(unmarshalled).as("Invalid result").isEqualTo(result);
} }
@Test
void fromTextMessageUntrusted() throws Exception {
converter = new MappingJackson2MessageConverter();
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 MappingJackson2MessageConverter();
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 MappingJackson2MessageConverter();
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 MappingJackson2MessageConverter();
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 MappingJackson2MessageConverter();
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 @Test
void fromTextMessageWithUnknownProperty() throws Exception { void fromTextMessageWithUnknownProperty() throws Exception {
TextMessage textMessageMock = mock(); TextMessage textMessageMock = mock();