From e36455c93e08ceab8a7ecdc2f2807b6ab7be583c Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Thu, 6 Mar 2025 16:23:53 +0200 Subject: [PATCH] Add protection against StackOverflowError in JsonValueWriter This commit adds validation for the maximum JSON nesting depth in the JsonValueWriter. This helps prevent StackOverflowError that can potentially occur due to excessive recursion when dealing with deeply nested JSON structures. Signed-off-by: Dmytro Nosan --- .../boot/json/JsonValueWriter.java | 23 ++++++++++++++ .../boot/json/JsonValueWriterTests.java | 31 +++++++++++++++++++ 2 files changed, 54 insertions(+) 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)); }