From b2e65f665aeaf991242272d13431bc2de742deff Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 22 Apr 2025 19:51:06 -0700 Subject: [PATCH 1/3] Polish --- .../org/springframework/boot/json/JsonValueWriter.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 1d8046a0f6..a48c304f4c 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 @@ -115,10 +115,7 @@ class JsonValueWriter { throw new UncheckedIOException(ex); } } - else if (value instanceof Path p) { - writeString(p.toString()); - } - else if (value instanceof Iterable iterable) { + else if (value instanceof Iterable iterable && canWriteAsArray(iterable)) { writeArray(iterable::forEach); } else if (ObjectUtils.isArray(value)) { @@ -135,6 +132,10 @@ class JsonValueWriter { } } + private boolean canWriteAsArray(Iterable iterable) { + return !(iterable instanceof Path); + } + /** * Start a new {@link Series} (JSON object or array). * @param series the series to start From b5e0eed8e7ec49cd8a9b1570d6b5f5be6a1bd3ad Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Thu, 6 Mar 2025 16:23:53 +0200 Subject: [PATCH 2/3] Protected against JsonValueWriter stack overflow Add validation for the maximum JSON nesting depth in the JsonValueWriter. This helps prevent a StackOverflowError that can potentially occur due to excessive recursion when dealing with deeply nested JSON structures. See gh-44627 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 a48c304f4c..27a5ee102a 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 @@ import org.springframework.util.function.ThrowingConsumer; */ 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) { @@ -145,6 +160,7 @@ class JsonValueWriter { */ void start(Series series) { if (series != null) { + validateNestingDepth(); this.activeSeries.push(new ActiveSeries(series)); append(series.openChar); } @@ -272,6 +288,13 @@ class JsonValueWriter { } } + 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 9de973d2fd..9986adc8de 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 @@ package org.springframework.boot.json; 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 @@ class JsonValueWriterTests { .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)); } From 4b607bde2c7da8a39c472b671a2aec5af81a10c3 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 22 Apr 2025 19:58:05 -0700 Subject: [PATCH 3/3] Polish 'Protected against JsonValueWriter stack overflow' See gh-44627 --- .../springframework/boot/json/JsonValueWriter.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) 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 27a5ee102a..cfefe35e6c 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,7 +47,7 @@ import org.springframework.util.function.ThrowingConsumer; */ class JsonValueWriter { - private static final int DEFAULT_MAX_NESTING_DEPTH = 1000; + private static final int DEFAULT_MAX_NESTING_DEPTH = 500; private final Appendable out; @@ -160,7 +160,10 @@ class JsonValueWriter { */ void start(Series series) { if (series != null) { - validateNestingDepth(); + int nestingDepth = this.activeSeries.size(); + Assert.state(nestingDepth <= this.maxNestingDepth, + () -> "JSON nesting depth (%s) exceeds maximum depth of %s (current path: %s)" + .formatted(nestingDepth, this.maxNestingDepth, this.path)); this.activeSeries.push(new ActiveSeries(series)); append(series.openChar); } @@ -288,13 +291,6 @@ class JsonValueWriter { } } - 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);