From ab8355faf5bc18000e8dc7574b8b59e430f03c6b Mon Sep 17 00:00:00 2001 From: Naman Agrawal <144216560+itsmehotpants@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:44:10 +0530 Subject: [PATCH] Add configurable structural nesting depth limit to SpEL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes spring-projects/spring-framework#36723 SpelParserConfiguration already enforces a maximum expression length and a maximum number of evaluation operations, but had no corresponding guard on structural nesting depth (e.g. deeply-nested inline lists and maps such as {{{{…}}}}). Without a bound, a crafted expression can drive the recursive-descent parser into arbitrarily deep Java call stacks, leading to StackOverflowError. Changes ------- SpelMessage • New entry MAX_NESTING_DEPTH_EXCEEDED (code 1086) used when the configured limit is breached during parsing. SpelParserConfiguration • DEFAULT_MAX_NESTING_DEPTH = 1_000 constant. • SPRING_EXPRESSION_MAX_NESTING_DEPTH_PROPERTY_NAME system-property constant ('spring.expression.maxNestingDepth') for global override. • maximumNestingDepth field with getter getMaximumNestingDepth(). • New canonical 8-arg constructor that accepts maximumNestingDepth; the previous 7-arg constructor now delegates to it (deprecated). • retrieveMaxNestingDepth() static helper reads the system property with the same validation pattern used by retrieveMaxOperations(). InternalSpelExpressionParser • nestingDepth counter field, reset to 0 at the start of each parse. • enterNesting(startPos) / exitNesting() helpers that increment / decrement the counter and throw SpelParseException (via internalException) when the limit is exceeded. • maybeEatInlineListOrMap() calls enterNesting() on LCURLY and exitNesting() before pushing the finished node, so every inline list and map contributes exactly one level of depth. SpelNestingDepthTests (new) • 15 JUnit 5 tests covering: configuration defaults, validation, lists, maps, mixed nesting, limit-boundary conditions, parser reusability after an exception, and error-message content. --- .../expression/spel/SpelMessage.java | 6 +- .../spel/SpelParserConfiguration.java | 83 ++++++- .../InternalSpelExpressionParser.java | 18 ++ .../spel/standard/SpelNestingDepthTests.java | 204 ++++++++++++++++++ 4 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelNestingDepthTests.java diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java index d58c89e7aea8..99e79ff57a36 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java @@ -307,7 +307,11 @@ public enum SpelMessage { /** @since 6.2.19 */ MAX_OPERATIONS_EXCEEDED(Kind.ERROR, 1085, - "SpEL expression evaluation exceeded the threshold of ''{0}'' operations"); + "SpEL expression evaluation exceeded the threshold of ''{0}'' operations"), + + /** @since 7.0 */ + MAX_NESTING_DEPTH_EXCEEDED(Kind.ERROR, 1086, + "SpEL expression structural nesting depth exceeded the threshold of ''{0}'' levels"); private final Kind kind; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java index aa2a3db8c740..6a26dc1d5d92 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java @@ -49,6 +49,15 @@ public class SpelParserConfiguration { */ public static final int DEFAULT_MAX_OPERATIONS = 10_000; + /** + * Default maximum structural nesting depth permitted for a SpEL expression: {@value}. + *
This guards against deeply nested inline lists, maps, and other recursive + * constructs that could cause excessive parser recursion. + * @since 7.0 + * @see #SPRING_EXPRESSION_MAX_NESTING_DEPTH_PROPERTY_NAME + */ + public static final int DEFAULT_MAX_NESTING_DEPTH = 1_000; + /** * System property to configure the default compiler mode for SpEL expression parsers: {@value}. *
NOTE: Instead of relying on a global default, applications @@ -74,6 +83,20 @@ public class SpelParserConfiguration { */ public static final String SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME = "spring.expression.maxOperations"; + /** + * System property to configure the default maximum structural nesting depth + * permitted for SpEL expression parsing: {@value}. + *
NOTE: Instead of relying on a global default, applications + * and frameworks should ideally set an explicit custom value via the + * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)} + * constructor which provides complete configuration control and the ability + * to override global defaults per use case. + *
Can also be configured via the {@link SpringProperties} mechanism.
+ * @since 7.0
+ * @see #DEFAULT_MAX_NESTING_DEPTH
+ */
+ public static final String SPRING_EXPRESSION_MAX_NESTING_DEPTH_PROPERTY_NAME = "spring.expression.maxNestingDepth";
+
private static final SpelCompilerMode defaultCompilerMode;
@@ -98,6 +121,8 @@ public class SpelParserConfiguration {
private final int maximumOperations;
+ private final int maximumNestingDepth;
+
/**
* Create a new {@code SpelParserConfiguration} instance with default settings.
@@ -206,7 +231,7 @@ public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullabl
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize, int maximumExpressionLength) {
this((compilerMode != null ? compilerMode : defaultCompilerMode), compilerClassLoader, autoGrowNullReferences,
- autoGrowCollections, maximumAutoGrowSize, maximumExpressionLength, retrieveMaxOperations());
+ autoGrowCollections, maximumAutoGrowSize, maximumExpressionLength, retrieveMaxOperations(), retrieveMaxNestingDepth());
}
/**
@@ -223,14 +248,43 @@ public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullabl
* @param maximumOperations the maximum number of operations permitted during
* SpEL expression evaluation; must be a positive number
* @since 6.2.19
+ * @deprecated as of 7.0, in favor of
+ * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)}
*/
+ @Deprecated(since = "7.0")
public SpelParserConfiguration(SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize, int maximumExpressionLength,
int maximumOperations) {
+ this(compilerMode, compilerClassLoader, autoGrowNullReferences, autoGrowCollections,
+ maximumAutoGrowSize, maximumExpressionLength, maximumOperations, retrieveMaxNestingDepth());
+ }
+
+ /**
+ * Create a new {@code SpelParserConfiguration} instance.
+ * @param compilerMode the compiler mode that parsers using this configuration
+ * should use; must not be {@code null}
+ * @param compilerClassLoader the {@code ClassLoader} to use as the basis for
+ * expression compilation; or {@code null} to use the default {@code ClassLoader}
+ * @param autoGrowNullReferences if null references should automatically grow
+ * @param autoGrowCollections if collections should automatically grow
+ * @param maximumAutoGrowSize the maximum size to which a collection can auto grow
+ * @param maximumExpressionLength the maximum length of a SpEL expression;
+ * must be a positive number
+ * @param maximumOperations the maximum number of operations permitted during
+ * SpEL expression evaluation; must be a positive number
+ * @param maximumNestingDepth the maximum structural nesting depth permitted
+ * during SpEL expression parsing; must be a positive number
+ * @since 7.0
+ */
+ public SpelParserConfiguration(SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
+ boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize, int maximumExpressionLength,
+ int maximumOperations, int maximumNestingDepth) {
+
Assert.notNull(compilerMode, "'compilerMode' must not be null");
Assert.isTrue(maximumExpressionLength > 0, "'maximumExpressionLength' must be a positive number");
Assert.isTrue(maximumOperations > 0, "'maximumOperations' must be a positive number");
+ Assert.isTrue(maximumNestingDepth > 0, "'maximumNestingDepth' must be a positive number");
this.compilerMode = compilerMode;
this.compilerClassLoader = compilerClassLoader;
@@ -239,6 +293,7 @@ public SpelParserConfiguration(SpelCompilerMode compilerMode, @Nullable ClassLoa
this.maximumAutoGrowSize = maximumAutoGrowSize;
this.maximumExpressionLength = maximumExpressionLength;
this.maximumOperations = maximumOperations;
+ this.maximumNestingDepth = maximumNestingDepth;
}
@@ -294,6 +349,14 @@ public int getMaximumOperations() {
return this.maximumOperations;
}
+ /**
+ * Return the maximum structural nesting depth permitted for a SpEL expression.
+ * @since 7.0
+ */
+ public int getMaximumNestingDepth() {
+ return this.maximumNestingDepth;
+ }
+
private static int retrieveMaxOperations() {
String value = SpringProperties.getProperty(SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME);
@@ -313,4 +376,22 @@ private static int retrieveMaxOperations() {
}
}
+ private static int retrieveMaxNestingDepth() {
+ String value = SpringProperties.getProperty(SPRING_EXPRESSION_MAX_NESTING_DEPTH_PROPERTY_NAME);
+ if (!StringUtils.hasText(value)) {
+ return DEFAULT_MAX_NESTING_DEPTH;
+ }
+
+ try {
+ int maxDepth = Integer.parseInt(value.trim());
+ Assert.isTrue(maxDepth > 0, () -> "Value [" + maxDepth + "] for system property [" +
+ SPRING_EXPRESSION_MAX_NESTING_DEPTH_PROPERTY_NAME + "] must be positive");
+ return maxDepth;
+ }
+ catch (NumberFormatException ex) {
+ throw new IllegalArgumentException("Failed to parse value for system property [" +
+ SPRING_EXPRESSION_MAX_NESTING_DEPTH_PROPERTY_NAME + "]: " + ex.getMessage(), ex);
+ }
+ }
+
}
diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java
index c94bea7277e5..3f38fc3d1558 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java
@@ -107,6 +107,9 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
// The expression being parsed
private String expressionString = "";
+ // Current structural nesting depth (inline lists, maps) while parsing
+ private int nestingDepth;
+
// The token stream constructed from that expression string
private List