diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/json/JsonValueWriter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/json/JsonValueWriter.java index 1d8046a0f604..9f3ca035119f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/json/JsonValueWriter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/json/JsonValueWriter.java @@ -47,8 +47,12 @@ */ class JsonValueWriter { + private static final int DEFAULT_MAX_NESTING_DEPTH = 1000; + private final Appendable out; + private final int maxNestingDepth; + private MemberPath path = MemberPath.ROOT; private final Deque filtersAndProcessors = new ArrayDeque<>(); @@ -60,7 +64,18 @@ class JsonValueWriter { * @param out the {@link Appendable} used to receive the JSON output */ JsonValueWriter(Appendable out) { + this(out, DEFAULT_MAX_NESTING_DEPTH); + } + + /** + * Create a new {@link JsonValueWriter} instance. + * @param out the {@link Appendable} used to receive the JSON output + * @param maxNestingDepth the maximum allowed nesting depth for JSON objects and + * arrays + */ + JsonValueWriter(Appendable out, int maxNestingDepth) { this.out = out; + this.maxNestingDepth = maxNestingDepth; } void pushProcessors(JsonWriterFiltersAndProcessors jsonProcessors) { @@ -144,6 +159,7 @@ else if (value instanceof Number || value instanceof Boolean) { */ void start(Series series) { if (series != null) { + validateNestingDepth(); this.activeSeries.push(new ActiveSeries(series)); append(series.openChar); } @@ -271,6 +287,13 @@ private void writeString(Object value) { } } + private void validateNestingDepth() { + if (this.activeSeries.size() > this.maxNestingDepth) { + throw new IllegalStateException("JSON nesting depth (%s) exceeds maximum depth of %s (current path: %s)" + .formatted(this.activeSeries.size(), this.maxNestingDepth, this.path)); + } + } + private void append(String value) { try { this.out.append(value); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/json/JsonValueWriterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/json/JsonValueWriterTests.java index 9de973d2fd1f..9986adc8defa 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/json/JsonValueWriterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/json/JsonValueWriterTests.java @@ -18,6 +18,7 @@ import java.io.File; import java.nio.file.Path; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -253,6 +254,36 @@ void writeJavaNioPathShouldBeSerializedAsString() { .isEqualTo(quoted("a\\%1$sb\\%1$sc".formatted(File.separator))); } + @Test + void illegalStateExceptionShouldBeThrownWhenCollectionExceededNestingDepth() { + JsonValueWriter writer = new JsonValueWriter(new StringBuilder(), 128); + List list = new ArrayList<>(); + list.add(list); + assertThatIllegalStateException().isThrownBy(() -> writer.write(list)) + .withMessageStartingWith( + "JSON nesting depth (129) exceeds maximum depth of 128 (current path: [0][0][0][0][0][0][0][0][0][0][0][0]"); + } + + @Test + void illegalStateExceptionShouldBeThrownWhenMapExceededNestingDepth() { + JsonValueWriter writer = new JsonValueWriter(new StringBuilder(), 128); + Map map = new LinkedHashMap<>(); + map.put("foo", Map.of("bar", map)); + assertThatIllegalStateException().isThrownBy(() -> writer.write(map)) + .withMessageStartingWith( + "JSON nesting depth (129) exceeds maximum depth of 128 (current path: foo.bar.foo.bar.foo.bar.foo"); + } + + @Test + void illegalStateExceptionShouldBeThrownWhenIterableExceededNestingDepth() { + JsonValueWriter writer = new JsonValueWriter(new StringBuilder(), 128); + List list = new ArrayList<>(); + list.add(list); + assertThatIllegalStateException().isThrownBy(() -> writer.write((Iterable) list::iterator)) + .withMessageStartingWith( + "JSON nesting depth (129) exceeds maximum depth of 128 (current path: [0][0][0][0][0][0][0][0][0][0][0][0]"); + } + private String write(V value) { return doWrite((valueWriter) -> valueWriter.write(value)); }