Skip to content

Commit 16c9e8f

Browse files
committed
spring-projectsGH-2687: Use JDK ObjectInputFilter for serialization security
Fixes: spring-projects#2687
1 parent 682aefe commit 16c9e8f

File tree

6 files changed

+56
-41
lines changed

6 files changed

+56
-41
lines changed

Diff for: spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java

+11-13
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
import java.io.ByteArrayInputStream;
2020
import java.io.ByteArrayOutputStream;
2121
import java.io.IOException;
22+
import java.io.ObjectInputFilter;
2223
import java.io.ObjectInputStream;
23-
import java.io.ObjectStreamClass;
2424
import java.io.UnsupportedEncodingException;
2525
import java.nio.charset.StandardCharsets;
2626

@@ -160,18 +160,16 @@ private Object asString(Message message, MessageProperties properties) {
160160
}
161161

162162
private Object deserialize(ByteArrayInputStream inputStream) throws IOException {
163-
try (ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream,
164-
this.defaultDeserializerClassLoader) {
165-
166-
@Override
167-
protected Class<?> resolveClass(ObjectStreamClass classDesc)
168-
throws IOException, ClassNotFoundException {
169-
Class<?> clazz = super.resolveClass(classDesc);
170-
checkAllowedList(clazz);
171-
return clazz;
172-
}
173-
174-
}) {
163+
ObjectInputStream objectInputStream =
164+
new ConfigurableObjectInputStream(inputStream, this.defaultDeserializerClassLoader);
165+
objectInputStream.setObjectInputFilter(
166+
ObjectInputFilter.allowFilter(aClass -> {
167+
checkAllowedList(aClass);
168+
return true;
169+
},
170+
ObjectInputFilter.Status.REJECTED));
171+
172+
try (objectInputStream) {
175173
return objectInputStream.readObject();
176174
}
177175
catch (ClassNotFoundException ex) {

Diff for: spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java

+9-11
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
import java.io.ByteArrayInputStream;
2020
import java.io.IOException;
2121
import java.io.InputStream;
22+
import java.io.ObjectInputFilter;
2223
import java.io.ObjectInputStream;
23-
import java.io.ObjectStreamClass;
2424
import java.io.Serializable;
2525
import java.io.UnsupportedEncodingException;
2626

@@ -152,16 +152,14 @@ else if (object instanceof Serializable) {
152152
* @throws IOException if creation of the ObjectInputStream failed
153153
*/
154154
protected ObjectInputStream createObjectInputStream(InputStream is) throws IOException {
155-
return new ConfigurableObjectInputStream(is, this.classLoader) {
156-
157-
@Override
158-
protected Class<?> resolveClass(ObjectStreamClass classDesc) throws IOException, ClassNotFoundException {
159-
Class<?> clazz = super.resolveClass(classDesc);
160-
checkAllowedList(clazz);
161-
return clazz;
162-
}
163-
164-
};
155+
ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(is, this.classLoader);
156+
objectInputStream.setObjectInputFilter(
157+
ObjectInputFilter.allowFilter(aClass -> {
158+
checkAllowedList(aClass);
159+
return true;
160+
},
161+
ObjectInputFilter.Status.REJECTED));
162+
return objectInputStream;
165163
}
166164

167165
}

Diff for: spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2023 the original author or authors.
2+
* Copyright 2006-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -145,6 +145,7 @@ protected Class<?> resolveClass(ObjectStreamClass classDesc)
145145
* Verify that the class is in the allowed list.
146146
* @param clazz the class.
147147
* @param patterns the patterns.
148+
* @throws SecurityException if class to deserialized is not allowed
148149
* @since 2.1
149150
*/
150151
public static void checkAllowedList(Class<?> clazz, Set<String> patterns) {

Diff for: spring-amqp/src/test/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverterTests.java

+18-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2023 the original author or authors.
2+
* Copyright 2016-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,22 +30,29 @@
3030

3131
/**
3232
* @author Gary Russell
33+
* @author Artem Bilan
3334
* @since 1.5.5
3435
*
3536
*/
3637
public class AllowedListDeserializingMessageConverterTests {
3738

3839
@Test
39-
public void testAllowedList() throws Exception {
40+
public void testAllowedList() {
4041
SerializerMessageConverter converter = new SerializerMessageConverter();
4142
TestBean testBean = new TestBean("foo");
4243
Message message = converter.toMessage(testBean, new MessageProperties());
43-
// when env var not set
44-
// assertThatExceptionOfType(SecurityException.class).isThrownBy(() -> converter.fromMessage(message));
4544
Object fromMessage;
46-
// when env var set.
47-
fromMessage = converter.fromMessage(message);
48-
assertThat(fromMessage).isEqualTo(testBean);
45+
// See build.gradle `tasks.withType(Test).all`
46+
if ("true".equals(System.getenv("SPRING_AMQP_DESERIALIZATION_TRUST_ALL"))) {
47+
fromMessage = converter.fromMessage(message);
48+
assertThat(fromMessage).isEqualTo(testBean);
49+
}
50+
else {
51+
assertThatExceptionOfType(MessageConversionException.class)
52+
.isThrownBy(() -> converter.fromMessage(message))
53+
.withRootCauseInstanceOf(SecurityException.class)
54+
.withStackTraceContaining("Attempt to deserialize unauthorized");
55+
}
4956

5057
converter.setAllowedListPatterns(Collections.singletonList("*"));
5158
fromMessage = converter.fromMessage(message);
@@ -59,7 +66,10 @@ public void testAllowedList() throws Exception {
5966
assertThat(fromMessage).isEqualTo(testBean);
6067

6168
converter.setAllowedListPatterns(Collections.singletonList("foo.*"));
62-
assertThatExceptionOfType(SecurityException.class).isThrownBy(() -> converter.fromMessage(message));
69+
assertThatExceptionOfType(MessageConversionException.class)
70+
.isThrownBy(() -> converter.fromMessage(message))
71+
.withRootCauseInstanceOf(SecurityException.class)
72+
.withStackTraceContaining("Attempt to deserialize unauthorized");
6373
}
6474

6575
@SuppressWarnings("serial")

Diff for: spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java

+3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.io.ObjectInputStream;
2626
import java.io.ObjectOutputStream;
2727
import java.nio.charset.StandardCharsets;
28+
import java.util.List;
2829

2930
import org.junit.jupiter.api.Test;
3031

@@ -74,6 +75,7 @@ public void messageToBytes() {
7475
@Test
7576
public void messageToSerializedObject() throws Exception {
7677
SerializerMessageConverter converter = new SerializerMessageConverter();
78+
converter.setAllowedListPatterns(List.of("*"));
7779
MessageProperties properties = new MessageProperties();
7880
properties.setContentType(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT);
7981
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
@@ -92,6 +94,7 @@ public void messageToSerializedObject() throws Exception {
9294
@Test
9395
public void messageToSerializedObjectNoContentType() throws Exception {
9496
SerializerMessageConverter converter = new SerializerMessageConverter();
97+
converter.setAllowedListPatterns(List.of(TestBean.class.getName()));
9598
converter.setIgnoreContentType(true);
9699
MessageProperties properties = new MessageProperties();
97100
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();

Diff for: spring-amqp/src/test/java/org/springframework/amqp/support/converter/SimpleMessageConverterTests.java

+13-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,6 +24,8 @@
2424
import java.io.ByteArrayOutputStream;
2525
import java.io.ObjectInputStream;
2626
import java.io.ObjectOutputStream;
27+
import java.nio.charset.StandardCharsets;
28+
import java.util.List;
2729

2830
import org.junit.jupiter.api.Test;
2931

@@ -33,27 +35,28 @@
3335
/**
3436
* @author Mark Fisher
3537
* @author Gary Russell
38+
* @author Artem Bilan
3639
*/
3740
public class SimpleMessageConverterTests extends AllowedListDeserializingMessageConverterTests {
3841

3942
@Test
40-
public void bytesAsDefaultMessageBodyType() throws Exception {
43+
public void bytesAsDefaultMessageBodyType() {
4144
SimpleMessageConverter converter = new SimpleMessageConverter();
4245
Message message = new Message("test".getBytes(), new MessageProperties());
4346
Object result = converter.fromMessage(message);
4447
assertThat(result.getClass()).isEqualTo(byte[].class);
45-
assertThat(new String((byte[]) result, "UTF-8")).isEqualTo("test");
48+
assertThat(new String((byte[]) result, StandardCharsets.UTF_8)).isEqualTo("test");
4649
}
4750

4851
@Test
49-
public void noMessageIdByDefault() throws Exception {
52+
public void noMessageIdByDefault() {
5053
SimpleMessageConverter converter = new SimpleMessageConverter();
5154
Message message = converter.toMessage("foo", null);
5255
assertThat(message.getMessageProperties().getMessageId()).isNull();
5356
}
5457

5558
@Test
56-
public void optionalMessageId() throws Exception {
59+
public void optionalMessageId() {
5760
SimpleMessageConverter converter = new SimpleMessageConverter();
5861
converter.setCreateMessageIds(true);
5962
Message message = converter.toMessage("foo", null);
@@ -87,6 +90,7 @@ public void messageToBytes() {
8790
@Test
8891
public void messageToSerializedObject() throws Exception {
8992
SimpleMessageConverter converter = new SimpleMessageConverter();
93+
converter.setAllowedListPatterns(List.of("*"));
9094
MessageProperties properties = new MessageProperties();
9195
properties.setContentType(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT);
9296
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
@@ -114,7 +118,7 @@ public void stringToMessage() throws Exception {
114118
}
115119

116120
@Test
117-
public void bytesToMessage() throws Exception {
121+
public void bytesToMessage() {
118122
SimpleMessageConverter converter = new SimpleMessageConverter();
119123
Message message = converter.toMessage(new byte[] { 1, 2, 3 }, new MessageProperties());
120124
String contentType = message.getMessageProperties().getContentType();
@@ -140,7 +144,7 @@ public void serializedObjectToMessage() throws Exception {
140144
}
141145

142146
@Test
143-
public void messageConversionExceptionForClassNotFound() throws Exception {
147+
public void messageConversionExceptionForClassNotFound() {
144148
SimpleMessageConverter converter = new SimpleMessageConverter();
145149
TestBean testBean = new TestBean("foo");
146150
Message message = converter.toMessage(testBean, new MessageProperties());
@@ -163,7 +167,8 @@ class Foo {
163167
fail("Expected exception");
164168
}
165169
catch (IllegalArgumentException e) {
166-
assertThat(e.getMessage()).contains("SimpleMessageConverter only supports String, byte[] and Serializable payloads, received:");
170+
assertThat(e.getMessage())
171+
.contains("SimpleMessageConverter only supports String, byte[] and Serializable payloads, received:");
167172
}
168173
}
169174

0 commit comments

Comments
 (0)