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-36791
This commit is contained in:
Sébastien Deleuze
2026-05-01 18:56:16 +02:00
committed by Brian Clozel
parent fe68e337b2
commit 9bec52b1ec
2 changed files with 119 additions and 0 deletions
@@ -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;
@@ -49,6 +50,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.
*
* <p>For untrusted environments, use {@link #setTrustedPackages(String...)}.
*
* <p>It customizes Jackson's default properties with the following ones:
* <ul>
* <li>{@link MapperFeature#DEFAULT_VIEW_INCLUSION} is disabled</li>
@@ -59,6 +62,7 @@ import org.springframework.util.ClassUtils;
* @author Dave Syer
* @author Juergen Hoeller
* @author Stephane Nicoll
* @author Sebastien Deleuze
* @since 3.1.4
* @deprecated since 7.0 in favor of {@link JacksonJsonMessageConverter}
*/
@@ -85,11 +89,14 @@ public class MappingJackson2MessageConverter implements SmartMessageConverter, B
private final Map<Class<?>, String> classIdMappings = new HashMap<>();
private String @Nullable [] trustedPackages;
private @Nullable ClassLoader beanClassLoader;
/**
* Construct a {@code MappingJackson2MessageConverter} with a default {@link ObjectMapper}.
* @see #setTrustedPackages(String...)
*/
@SuppressWarnings("deprecation") // on Jackson 2.13: configure(MapperFeature, boolean)
public MappingJackson2MessageConverter() {
@@ -102,12 +109,21 @@ public class MappingJackson2MessageConverter implements SmartMessageConverter, B
* Construct a {@code MappingJackson2MessageConverter} with a custom {@link ObjectMapper}.
* @param objectMapper the {@code ObjectMapper} to use
* @since 6.1
* @see #setTrustedPackages(String...)
*/
public MappingJackson2MessageConverter(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
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.
@@ -181,6 +197,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
public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = classLoader;
@@ -461,6 +494,10 @@ public class MappingJackson2MessageConverter implements SmartMessageConverter, B
if (mappedClass != null) {
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 {
Class<?> typeClass = ClassUtils.forName(typeId, this.beanClassLoader);
return this.objectMapper.constructType(typeClass);
@@ -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;
@@ -48,6 +49,7 @@ import static org.mockito.Mockito.verify;
* @author Arjen Poutsma
* @author Dave Syer
* @author Stephane Nicoll
* @author Sebastien Deleuze
*/
@SuppressWarnings("removal")
class MappingJackson2MessageConverterTests {
@@ -134,6 +136,86 @@ class MappingJackson2MessageConverterTests {
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
void fromTextMessageWithUnknownProperty() throws Exception {
TextMessage textMessageMock = mock();