Skip to content

Commit 6966ebd

Browse files
committed
Protect against deeply nested JSON lists
Update `BasicJsonParser` to protect against deeply nested JSON lists in the same way as Jackson. Fixes gh-31868
1 parent 24c4ba3 commit 6966ebd

File tree

5 files changed

+42
-6
lines changed

5 files changed

+42
-6
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/json/BasicJsonParser.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,28 +37,33 @@
3737
*/
3838
public class BasicJsonParser extends AbstractJsonParser {
3939

40+
private static final int MAX_DEPTH = 1000;
41+
4042
@Override
4143
public Map<String, Object> parseMap(String json) {
4244
return tryParse(() -> parseMap(json, this::parseMapInternal), Exception.class);
4345
}
4446

4547
@Override
4648
public List<Object> parseList(String json) {
47-
return tryParse(() -> parseList(json, this::parseListInternal), Exception.class);
49+
return tryParse(() -> parseList(json, (jsonToParse) -> parseListInternal(0, jsonToParse)), Exception.class);
4850
}
4951

50-
private List<Object> parseListInternal(String json) {
52+
private List<Object> parseListInternal(int nesting, String json) {
5153
List<Object> list = new ArrayList<>();
5254
json = trimLeadingCharacter(trimTrailingCharacter(json, ']'), '[').trim();
5355
for (String value : tokenize(json)) {
54-
list.add(parseInternal(value));
56+
list.add(parseInternal(nesting + 1, value));
5557
}
5658
return list;
5759
}
5860

59-
private Object parseInternal(String json) {
61+
private Object parseInternal(int nesting, String json) {
62+
if (nesting > MAX_DEPTH) {
63+
throw new IllegalStateException("JSON is too deeply nested");
64+
}
6065
if (json.startsWith("[")) {
61-
return parseListInternal(json);
66+
return parseListInternal(nesting + 1, json);
6267
}
6368
if (json.startsWith("{")) {
6469
return parseMapInternal(json);
@@ -101,7 +106,7 @@ private Map<String, Object> parseMapInternal(String json) {
101106
for (String pair : tokenize(json)) {
102107
String[] values = StringUtils.trimArrayElements(StringUtils.split(pair, ":"));
103108
String key = trimLeadingCharacter(trimTrailingCharacter(values[0], '"'), '"');
104-
Object value = parseInternal(values[1]);
109+
Object value = parseInternal(0, values[1]);
105110
map.put(key, value);
106111
}
107112
return map;

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/json/AbstractJsonParserTests.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@
1616

1717
package org.springframework.boot.json;
1818

19+
import java.io.IOException;
20+
import java.nio.charset.StandardCharsets;
1921
import java.util.List;
2022
import java.util.Map;
2123

2224
import org.junit.jupiter.api.Test;
2325

26+
import org.springframework.util.StreamUtils;
27+
2428
import static org.assertj.core.api.Assertions.assertThat;
2529
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2630

@@ -186,4 +190,12 @@ void mapWithKeyAndNoValue() {
186190
assertThatExceptionOfType(JsonParseException.class).isThrownBy(() -> this.parser.parseMap("{\"foo\"}"));
187191
}
188192

193+
@Test // gh-31868
194+
void listWithRepeatedOpenArray() throws IOException {
195+
String input = StreamUtils.copyToString(
196+
AbstractJsonParserTests.class.getResourceAsStream("repeated-open-array.txt"), StandardCharsets.UTF_8);
197+
assertThatExceptionOfType(JsonParseException.class).isThrownBy(() -> this.parser.parseList(input)).havingCause()
198+
.withMessageContaining("too deeply nested");
199+
}
200+
189201
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/json/GsonJsonParserTests.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
package org.springframework.boot.json;
1818

19+
import java.io.IOException;
20+
21+
import org.junit.jupiter.api.Disabled;
22+
1923
/**
2024
* Tests for {@link GsonJsonParser}.
2125
*
@@ -28,4 +32,10 @@ protected JsonParser getParser() {
2832
return new GsonJsonParser();
2933
}
3034

35+
@Override
36+
@Disabled("Gson does not protect against deeply nested JSON")
37+
void listWithRepeatedOpenArray() throws IOException {
38+
super.listWithRepeatedOpenArray();
39+
}
40+
3141
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/json/YamlJsonParserTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.boot.json;
1818

19+
import java.io.IOException;
20+
1921
import org.junit.jupiter.api.Disabled;
2022
import org.junit.jupiter.api.Test;
2123
import org.yaml.snakeyaml.constructor.ConstructorException;
@@ -53,4 +55,10 @@ void listWithMalformedMap() {
5355
void mapWithKeyAndNoValue() {
5456
}
5557

58+
@Override
59+
@Disabled("SnakeYaml does not protect against deeply nested JSON")
60+
void listWithRepeatedOpenArray() throws IOException {
61+
super.listWithRepeatedOpenArray();
62+
}
63+
5664
}

spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/json/repeated-open-array.txt

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)