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 tokenStream = Collections.emptyList();
@@ -134,6 +137,7 @@ protected SpelExpression doParseExpression(String expressionString, @Nullable Pa
try {
this.expressionString = expressionString;
+ this.nestingDepth = 0;
Tokenizer tokenizer = new Tokenizer(expressionString);
this.tokenStream = tokenizer.process();
this.tokenStreamLength = this.tokenStream.size();
@@ -161,6 +165,18 @@ private void checkExpressionLength(String string) {
}
}
+ private void enterNesting(int startPos) {
+ this.nestingDepth++;
+ int maxDepth = this.configuration.getMaximumNestingDepth();
+ if (this.nestingDepth > maxDepth) {
+ throw internalException(startPos, SpelMessage.MAX_NESTING_DEPTH_EXCEEDED, maxDepth);
+ }
+ }
+
+ private void exitNesting() {
+ this.nestingDepth--;
+ }
+
// expression
// : logicalOrExpression
// ( (ASSIGN^ logicalOrExpression)
@@ -636,6 +652,7 @@ private boolean maybeEatInlineListOrMap() {
if (t == null || !peekToken(TokenKind.LCURLY, true)) {
return false;
}
+ enterNesting(t.startPos);
SpelNodeImpl expr = null;
Token closingCurly = peekToken();
if (closingCurly != null && peekToken(TokenKind.RCURLY, true)) {
@@ -686,6 +703,7 @@ else if (peekToken(TokenKind.COLON, true)) { // map!
throw internalException(t.startPos, SpelMessage.OOD);
}
}
+ exitNesting();
this.constructedNodes.push(expr);
return true;
}
diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelNestingDepthTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelNestingDepthTests.java
new file mode 100644
index 000000000000..33f7818809bb
--- /dev/null
+++ b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelNestingDepthTests.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2002-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.expression.spel.standard;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.expression.spel.SpelMessage;
+import org.springframework.expression.spel.SpelParseException;
+import org.springframework.expression.spel.SpelParserConfiguration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for the configurable structural nesting depth limit in SpEL expressions.
+ *
+ * @author Naman Agrawal
+ * @since 7.0
+ * @see SpelParserConfiguration#DEFAULT_MAX_NESTING_DEPTH
+ * @see SpelParserConfiguration#SPRING_EXPRESSION_MAX_NESTING_DEPTH_PROPERTY_NAME
+ * @see SpelMessage#MAX_NESTING_DEPTH_EXCEEDED
+ */
+class SpelNestingDepthTests {
+
+ // ------------------------------------------------------------------
+ // SpelParserConfiguration defaults and accessors
+ // ------------------------------------------------------------------
+
+ @Test
+ void defaultMaxNestingDepthConstantIsPositive() {
+ assertThat(SpelParserConfiguration.DEFAULT_MAX_NESTING_DEPTH).isPositive();
+ }
+
+ @Test
+ void defaultConfigurationReportsDefaultNestingDepth() {
+ SpelParserConfiguration config = new SpelParserConfiguration();
+ assertThat(config.getMaximumNestingDepth())
+ .isEqualTo(SpelParserConfiguration.DEFAULT_MAX_NESTING_DEPTH);
+ }
+
+ @Test
+ void customNestingDepthIsStoredCorrectly() {
+ SpelParserConfiguration config = new SpelParserConfiguration(
+ null, null, false, false, Integer.MAX_VALUE,
+ SpelParserConfiguration.DEFAULT_MAX_EXPRESSION_LENGTH,
+ SpelParserConfiguration.DEFAULT_MAX_OPERATIONS, 42);
+ assertThat(config.getMaximumNestingDepth()).isEqualTo(42);
+ }
+
+ @Test
+ void nestingDepthMustBePositive() {
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> new SpelParserConfiguration(
+ null, null, false, false, Integer.MAX_VALUE,
+ SpelParserConfiguration.DEFAULT_MAX_EXPRESSION_LENGTH,
+ SpelParserConfiguration.DEFAULT_MAX_OPERATIONS, 0))
+ .withMessageContaining("maximumNestingDepth");
+ }
+
+ // ------------------------------------------------------------------
+ // Inline lists
+ // ------------------------------------------------------------------
+
+ @Test
+ void flatInlineListWithinDepthLimit() {
+ SpelExpressionParser parser = parserWithMaxDepth(3);
+ // {1, 2, 3} – depth 1, should parse fine
+ assertThat(parser.parseExpression("{1, 2, 3}")).isNotNull();
+ }
+
+ @Test
+ void nestedInlineListAtExactLimit() {
+ SpelExpressionParser parser = parserWithMaxDepth(3);
+ // {{{}}} – depth 3, exactly at the limit
+ assertThat(parser.parseExpression("{{{}}}")).isNotNull();
+ }
+
+ @Test
+ void nestedInlineListExceedingDepthThrowsException() {
+ SpelExpressionParser parser = parserWithMaxDepth(3);
+ // {{{{}}}} – depth 4, one level over
+ assertThatExceptionOfType(SpelParseException.class)
+ .isThrownBy(() -> parser.parseExpression("{{{{}}}}"))
+ .satisfies(ex -> assertThat(ex.getMessageCode())
+ .isEqualTo(SpelMessage.MAX_NESTING_DEPTH_EXCEEDED));
+ }
+
+ @Test
+ void deeplyNestedInlineListExceedsDefaultLimitEventually() {
+ SpelExpressionParser parser = new SpelExpressionParser();
+ String expr = buildNestedList(SpelParserConfiguration.DEFAULT_MAX_NESTING_DEPTH + 1);
+ assertThatExceptionOfType(SpelParseException.class)
+ .isThrownBy(() -> parser.parseExpression(expr))
+ .satisfies(ex -> assertThat(ex.getMessageCode())
+ .isEqualTo(SpelMessage.MAX_NESTING_DEPTH_EXCEEDED));
+ }
+
+ // ------------------------------------------------------------------
+ // Inline maps
+ // ------------------------------------------------------------------
+
+ @Test
+ void flatInlineMapWithinDepthLimit() {
+ SpelExpressionParser parser = parserWithMaxDepth(3);
+ // {'k': 'v'} – depth 1
+ assertThat(parser.parseExpression("{'k': 'v'}")).isNotNull();
+ }
+
+ @Test
+ void nestedInlineMapAtExactLimit() {
+ SpelExpressionParser parser = parserWithMaxDepth(3);
+ // {'a': {'b': {'c': 1}}} – depth 3
+ assertThat(parser.parseExpression("{'a': {'b': {'c': 1}}}")).isNotNull();
+ }
+
+ @Test
+ void nestedInlineMapExceedingDepthThrowsException() {
+ SpelExpressionParser parser = parserWithMaxDepth(2);
+ // {'a': {'b': {'c': 1}}} – depth 3, over the limit of 2
+ assertThatExceptionOfType(SpelParseException.class)
+ .isThrownBy(() -> parser.parseExpression("{'a': {'b': {'c': 1}}}"))
+ .satisfies(ex -> assertThat(ex.getMessageCode())
+ .isEqualTo(SpelMessage.MAX_NESTING_DEPTH_EXCEEDED));
+ }
+
+ // ------------------------------------------------------------------
+ // Mixed list + map nesting
+ // ------------------------------------------------------------------
+
+ @Test
+ void mixedNestingAtExactLimit() {
+ SpelExpressionParser parser = parserWithMaxDepth(2);
+ // {{'k': 'v'}} – depth 2
+ assertThat(parser.parseExpression("{{'k': 'v'}}")).isNotNull();
+ }
+
+ @Test
+ void mixedNestingExceedingDepthThrowsException() {
+ SpelExpressionParser parser = parserWithMaxDepth(2);
+ // {{{'k': 'v'}}} – depth 3
+ assertThatExceptionOfType(SpelParseException.class)
+ .isThrownBy(() -> parser.parseExpression("{{{'k': 'v'}}}"))
+ .satisfies(ex -> assertThat(ex.getMessageCode())
+ .isEqualTo(SpelMessage.MAX_NESTING_DEPTH_EXCEEDED));
+ }
+
+ // ------------------------------------------------------------------
+ // Re-use of parser across multiple expressions (counter must reset)
+ // ------------------------------------------------------------------
+
+ @Test
+ void parserIsReusableAfterExceedingDepth() {
+ SpelExpressionParser parser = parserWithMaxDepth(1);
+
+ // First parse: exceeds depth
+ assertThatExceptionOfType(SpelParseException.class)
+ .isThrownBy(() -> parser.parseExpression("{{1}}"));
+
+ // Second parse: within depth – must succeed
+ assertThat(parser.parseExpression("{1}")).isNotNull();
+ }
+
+ @Test
+ void errorMessageContainsConfiguredLimit() {
+ int limit = 2;
+ SpelExpressionParser parser = parserWithMaxDepth(limit);
+ assertThatExceptionOfType(SpelParseException.class)
+ .isThrownBy(() -> parser.parseExpression("{{{1}}}"))
+ .withMessageContaining(String.valueOf(limit));
+ }
+
+ // ------------------------------------------------------------------
+ // Helpers
+ // ------------------------------------------------------------------
+
+ private static SpelExpressionParser parserWithMaxDepth(int maxDepth) {
+ SpelParserConfiguration config = new SpelParserConfiguration(
+ null, null, false, false, Integer.MAX_VALUE,
+ SpelParserConfiguration.DEFAULT_MAX_EXPRESSION_LENGTH,
+ SpelParserConfiguration.DEFAULT_MAX_OPERATIONS, maxDepth);
+ return new SpelExpressionParser(config);
+ }
+
+ /** Build a string like {{...{}}...}} with {@code depth} levels of nesting. */
+ private static String buildNestedList(int depth) {
+ return "{".repeat(depth) + "}".repeat(depth);
+ }
+
+}