Add trusted packages to JacksonJsonMessageConverter

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.

See gh-36791
This commit is contained in:
Sébastien Deleuze
2026-05-01 17:26:16 +02:00
committed by Brian Clozel
parent 322eef2942
commit fe68e337b2
2 changed files with 121 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;
@@ -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.
*
* <p>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<Class<?>, 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);
@@ -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();