Add a configurable limit for maximum nesting depth in SpEL expressions

This commit introduces support for limiting the structural nesting
depth of a SpEL expression during parsing. Without such a limit, an
expression with deeply nested constructs (for example, inline lists or
maps, parenthesized expressions, ternary or Elvis expressions, or
chained unary operators) can cause SpEL's recursive-descent parser to
throw a StackOverflowError which lacks useful diagnostics for
developer's attempting to assess what went wrong.

With this commit, a nesting-depth counter is now tracked around the
parser's primary recursive entry point (eatExpression()) as well as
around chained unary operators (eatUnaryExpression()), ensuring that
independent, sibling uses of assignment, Elvis, and ternary expressions
do not inadvertently accumulate depth and trip the limit.

If the configured (or default) nesting-depth limit is exceeded during
parsing, a SpelParseException is thrown instead, with a message that
reports the configured limit.

The limit can be configured on a per-use-case basis via
SpelParserConfiguration and defaults to 1000.

Closes gh-36723
This commit is contained in:
Sam Brannen
2026-08-21 13:15:14 +02:00
parent 89047909ea
commit 8473ec3e25
5 changed files with 297 additions and 44 deletions
@@ -588,6 +588,31 @@ JVM system property or Spring property named `spring.expression.maxBigPowerBits`
maximum result size in bits (see xref:appendix.adoc#appendix-spring-properties[Supported
Spring Properties]).
Likewise, the structural nesting depth of a SpEL expression -- for example, the depth of
nested inline lists or maps, parenthesized expressions, ternary or Elvis expressions, or
chained unary operators -- cannot exceed 1,000 by default; however, the
`maximumNestingDepth` value is configurable. If you create a `SpelExpressionParser`
programmatically, you can specify a custom `maximumNestingDepth` value when creating the
`SpelParserConfiguration` that you provide to the `SpelExpressionParser`. Unlike
`maxExpressionLength` and `maxOperations`, there is currently no JVM system property or
Spring property available for configuring `maximumNestingDepth` globally.
[NOTE]
====
Without such a limit, a sufficiently deeply nested expression can drive SpEL's
recursive-descent parser to exhaust the current thread's call stack, resulting in a
`StackOverflowError` instead of a descriptive exception.
The `maximumNestingDepth` limit improves diagnostics for that common case by converting
it into a clear `SpelParseException`; however, it is not a guaranteed defense against
`StackOverflowError` under every possible JVM thread stack size configuration, since the
amount of stack space consumed per level of nesting depends on the JVM, its JIT
compilation state, and the platform. Applications and frameworks that evaluate SpEL
expressions from an untrusted source should not rely on `maximumNestingDepth` alone and
should instead heed the <<expressions-evaluation-context-security,security
considerations>> discussed previously in this chapter.
====
[[expressions-spel-compilation]]
== SpEL Compilation
@@ -312,7 +312,11 @@ public enum SpelMessage {
/** @since 7.0.9 */
MAX_BIG_POWER_RESULT_EXCEEDED(Kind.ERROR, 1086,
"BigDecimal/BigInteger power operation with base bit length ''{0}'' and exponent ''{1}'' " +
"would produce a result exceeding the configured maximum of ''{2}'' bits");
"would produce a result exceeding the configured maximum of ''{2}'' bits"),
/** @since 7.1 */
MAX_EXPRESSION_NESTING_DEPTH_EXCEEDED(Kind.ERROR, 1087,
"SpEL expression nesting depth exceeds the threshold of {0}");
private final Kind kind;
@@ -59,11 +59,35 @@ public class SpelParserConfiguration {
*/
public static final int DEFAULT_MAX_BIG_POWER_BITS = 1_000_000;
/**
* Default maximum nesting depth permitted within a SpEL expression: {@value}.
* <p>This limit guards against deeply nested constructs (for example, nested
* inline lists or maps, parenthesized expressions, ternary or Elvis expressions,
* or chained unary operators) that could otherwise drive SpEL's recursive-descent
* parser to exhaust the current thread's call stack.
* <p><strong>NOTE</strong>: This limit improves diagnostics for the common case
* by converting what would otherwise be an opaque {@link StackOverflowError}
* into a descriptive {@link SpelParseException}, but it is <em>not</em> a
* guaranteed defense against {@code StackOverflowError} under every possible
* JVM thread stack size configuration. The amount of stack space consumed per
* level of nesting depends on the JVM, its current JIT compilation state, and
* the platform; consequently, this default may not suffice on threads configured
* with a substantially reduced stack size (for example, via a reduced {@code -Xss}
* setting, as is sometimes done in high-concurrency deployments to support large
* thread pools). Applications and frameworks that evaluate SpEL expressions from
* an untrusted source should not rely on this limit alone; see the
* <a href="https://docs.spring.io/spring-framework/reference/core/expressions/evaluation.html#expressions-evaluation-context-security"
* >Security Considerations</a> section of the Spring Framework reference
* documentation for further guidance on evaluating untrusted SpEL expressions.
* @since 7.1
*/
public static final int DEFAULT_MAX_EXPRESSION_NESTING_DEPTH = 1_000;
/**
* System property to configure the default compiler mode for SpEL expression parsers: {@value}.
* <p><strong>NOTE</strong>: 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)}
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int, int)}
* constructor which provides complete configuration control and the ability
* to override global defaults per use case.
* <p>Can also be configured via the {@link SpringProperties} mechanism.
@@ -75,7 +99,7 @@ public class SpelParserConfiguration {
* during SpEL expression evaluation: {@value}.
* <p><strong>NOTE</strong>: 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)}
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int, int)}
* constructor which provides complete configuration control and the ability
* to override global defaults per use case.
* <p>Can also be configured via the {@link SpringProperties} mechanism.
@@ -90,7 +114,7 @@ public class SpelParserConfiguration {
* operation within a SpEL expression: {@value}.
* <p><strong>NOTE</strong>: 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)}
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int, int)}
* constructor which provides complete configuration control and the ability
* to override global defaults per use case.
* <p>Can also be configured via the {@link SpringProperties} mechanism.
@@ -126,11 +150,13 @@ public class SpelParserConfiguration {
private final int maximumBigPowerBits;
private final int maximumNestingDepth;
/**
* Create a new {@code SpelParserConfiguration} instance with default settings.
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)}
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
@@ -144,7 +170,7 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)}
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param compilerMode the compiler mode that parsers using this configuration
@@ -162,7 +188,7 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)}
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param autoGrowNullReferences if null references should automatically grow
@@ -178,7 +204,7 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)}
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param autoGrowNullReferences if null references should automatically grow
@@ -195,7 +221,7 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)}
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param compilerMode the compiler mode that parsers using this configuration
@@ -219,7 +245,7 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)}
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param compilerMode the compiler mode that parsers using this configuration
@@ -235,6 +261,7 @@ public class SpelParserConfiguration {
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_BIG_POWER_BITS_PROPERTY_NAME
* @see #DEFAULT_MAX_EXPRESSION_NESTING_DEPTH
*/
public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize, int maximumExpressionLength) {
@@ -246,7 +273,7 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)}
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param compilerMode the compiler mode that parsers using this configuration
@@ -262,6 +289,7 @@ public class SpelParserConfiguration {
* SpEL expression evaluation; must be a positive number
* @since 6.2.19
* @see #SPRING_EXPRESSION_MAX_BIG_POWER_BITS_PROPERTY_NAME
* @see #DEFAULT_MAX_EXPRESSION_NESTING_DEPTH
*/
public SpelParserConfiguration(SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize, int maximumExpressionLength,
@@ -271,6 +299,37 @@ public class SpelParserConfiguration {
maximumAutoGrowSize, maximumExpressionLength, maximumOperations, retrieveMaxBigPowerBits());
}
/**
* Create a new {@code SpelParserConfiguration} instance.
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @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 maximumBigPowerBits the maximum number of bits permitted in the
* result of a {@link java.math.BigDecimal} or {@link java.math.BigInteger} power
* operation; must be a positive number; use {@link Integer#MAX_VALUE} for no limit
* @since 7.0.9
* @see #DEFAULT_MAX_EXPRESSION_NESTING_DEPTH
*/
public SpelParserConfiguration(SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize, int maximumExpressionLength,
int maximumOperations, int maximumBigPowerBits) {
this(compilerMode, compilerClassLoader, autoGrowNullReferences, autoGrowCollections, maximumAutoGrowSize,
maximumExpressionLength, maximumOperations, maximumBigPowerBits, DEFAULT_MAX_EXPRESSION_NESTING_DEPTH);
}
/**
* Create a new {@code SpelParserConfiguration} instance.
* @param compilerMode the compiler mode that parsers using this configuration
@@ -287,16 +346,20 @@ public class SpelParserConfiguration {
* @param maximumBigPowerBits the maximum number of bits permitted in the
* result of a {@link java.math.BigDecimal} or {@link java.math.BigInteger} power
* operation; must be a positive number; use {@link Integer#MAX_VALUE} for no limit
* @since 7.0.9
* @param maximumNestingDepth the maximum nesting depth permitted within a SpEL
* expression; must be a positive number
* @since 7.1
* @see #DEFAULT_MAX_EXPRESSION_NESTING_DEPTH
*/
public SpelParserConfiguration(SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize, int maximumExpressionLength,
int maximumOperations, int maximumBigPowerBits) {
int maximumOperations, int maximumBigPowerBits, 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(maximumBigPowerBits > 0, "'maximumBigPowerBits' must be a positive number");
Assert.isTrue(maximumNestingDepth > 0, "'maximumNestingDepth' must be a positive number");
this.compilerMode = compilerMode;
this.compilerClassLoader = compilerClassLoader;
@@ -306,6 +369,7 @@ public class SpelParserConfiguration {
this.maximumExpressionLength = maximumExpressionLength;
this.maximumOperations = maximumOperations;
this.maximumBigPowerBits = maximumBigPowerBits;
this.maximumNestingDepth = maximumNestingDepth;
}
@@ -370,6 +434,15 @@ public class SpelParserConfiguration {
return this.maximumBigPowerBits;
}
/**
* Return the maximum nesting depth permitted within a SpEL expression.
* @since 7.1
* @see #DEFAULT_MAX_EXPRESSION_NESTING_DEPTH
*/
public int getMaximumNestingDepth() {
return this.maximumNestingDepth;
}
private static int retrieveMaxOperations() {
String value = SpringProperties.getProperty(SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME);
@@ -107,6 +107,8 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
// The expression being parsed
private String expressionString = "";
private int nestingDepth = 0;
// The token stream constructed from that expression string
private List<Token> tokenStream = Collections.emptyList();
@@ -169,40 +171,46 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
// | (ELVIS^ expression))?;
@SuppressWarnings("NullAway") // Not null assertion performed in SpelNodeImpl constructor
private @Nullable SpelNodeImpl eatExpression() {
SpelNodeImpl expr = eatLogicalOrExpression();
Token t = peekToken();
if (t != null) {
if (t.kind == TokenKind.ASSIGN) { // a=b
if (expr == null) {
expr = new NullLiteral(t.startPos - 1, t.endPos - 1);
incrementNestingDepth();
try {
SpelNodeImpl expr = eatLogicalOrExpression();
Token t = peekToken();
if (t != null) {
if (t.kind == TokenKind.ASSIGN) { // a=b
if (expr == null) {
expr = new NullLiteral(t.startPos - 1, t.endPos - 1);
}
nextToken();
SpelNodeImpl assignedValue = eatLogicalOrExpression();
return new Assign(t.startPos, t.endPos, expr, assignedValue);
}
nextToken();
SpelNodeImpl assignedValue = eatLogicalOrExpression();
return new Assign(t.startPos, t.endPos, expr, assignedValue);
}
if (t.kind == TokenKind.ELVIS) { // a?:b (a if it isn't null, otherwise b)
if (expr == null) {
expr = new NullLiteral(t.startPos - 1, t.endPos - 2);
if (t.kind == TokenKind.ELVIS) { // a?:b (a if it isn't null, otherwise b)
if (expr == null) {
expr = new NullLiteral(t.startPos - 1, t.endPos - 2);
}
nextToken(); // elvis has left the building
SpelNodeImpl valueIfNull = eatExpression();
if (valueIfNull == null) {
valueIfNull = new NullLiteral(t.startPos + 1, t.endPos + 1);
}
return new Elvis(t.startPos, t.endPos, expr, valueIfNull);
}
nextToken(); // elvis has left the building
SpelNodeImpl valueIfNull = eatExpression();
if (valueIfNull == null) {
valueIfNull = new NullLiteral(t.startPos + 1, t.endPos + 1);
if (t.kind == TokenKind.QMARK) { // a?b:c
if (expr == null) {
expr = new NullLiteral(t.startPos - 1, t.endPos - 1);
}
nextToken();
SpelNodeImpl ifTrueExprValue = eatExpression();
eatToken(TokenKind.COLON);
SpelNodeImpl ifFalseExprValue = eatExpression();
return new Ternary(t.startPos, t.endPos, expr, ifTrueExprValue, ifFalseExprValue);
}
return new Elvis(t.startPos, t.endPos, expr, valueIfNull);
}
if (t.kind == TokenKind.QMARK) { // a?b:c
if (expr == null) {
expr = new NullLiteral(t.startPos - 1, t.endPos - 1);
}
nextToken();
SpelNodeImpl ifTrueExprValue = eatExpression();
eatToken(TokenKind.COLON);
SpelNodeImpl ifFalseExprValue = eatExpression();
return new Ternary(t.startPos, t.endPos, expr, ifTrueExprValue, ifFalseExprValue);
}
return expr;
}
finally {
decrementNestingDepth();
}
return expr;
}
//logicalOrExpression : logicalAndExpression (OR^ logicalAndExpression)*;
@@ -336,7 +344,7 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
private @Nullable SpelNodeImpl eatUnaryExpression() {
if (peekToken(TokenKind.NOT, TokenKind.PLUS, TokenKind.MINUS)) {
Token t = takeToken();
SpelNodeImpl expr = eatUnaryExpression();
SpelNodeImpl expr = eatUnaryOperand();
if (expr == null) {
throw internalException(t.startPos, SpelMessage.OOD);
}
@@ -352,7 +360,7 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
}
if (peekToken(TokenKind.INC, TokenKind.DEC)) {
Token t = takeToken();
SpelNodeImpl expr = eatUnaryExpression();
SpelNodeImpl expr = eatUnaryOperand();
if (t.getKind() == TokenKind.INC) {
return new OpInc(t.startPos, t.endPos, false, expr);
}
@@ -363,6 +371,24 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
return eatPrimaryExpression();
}
/**
* Eat the operand of a chained unary operator (for example, {@code !} or
* {@code -}), tracking nesting depth so that a long chain of unary operators
* cannot drive the recursive-descent parser into a {@link StackOverflowError}.
* @since 7.1
* @see #eatUnaryExpression()
* @see SpelParserConfiguration#getMaximumNestingDepth()
*/
private @Nullable SpelNodeImpl eatUnaryOperand() {
incrementNestingDepth();
try {
return eatUnaryExpression();
}
finally {
decrementNestingDepth();
}
}
// primaryExpression : startNode (node)? -> ^(EXPRESSION startNode (node)?);
private @Nullable SpelNodeImpl eatPrimaryExpression() {
SpelNodeImpl start = eatStartNode(); // always a start node
@@ -1054,6 +1080,28 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
}
}
/**
* Increment the nesting depth.
* @since 7.1
* @see SpelParserConfiguration#getMaximumNestingDepth()
*/
private void incrementNestingDepth() {
int maxNestingDepth = this.configuration.getMaximumNestingDepth();
if (this.nestingDepth++ > maxNestingDepth) {
throw new InternalParseException(new SpelParseException(0,
SpelMessage.MAX_EXPRESSION_NESTING_DEPTH_EXCEEDED, maxNestingDepth));
}
}
/**
* Decrement the nesting depth.
* @since 7.1
* @see SpelParserConfiguration#getMaximumNestingDepth()
*/
private void decrementNestingDepth() {
this.nestingDepth--;
}
private InternalParseException internalException(int startPos, SpelMessage message, Object... inserts) {
return new InternalParseException(new SpelParseException(this.expressionString, startPos, message, inserts));
}
@@ -20,13 +20,16 @@ import java.util.function.Consumer;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.assertj.core.api.ThrowableAssertAlternative;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionException;
import org.springframework.expression.spel.SpelCompilerMode;
import org.springframework.expression.spel.SpelMessage;
import org.springframework.expression.spel.SpelNode;
import org.springframework.expression.spel.SpelParseException;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.ast.OpAnd;
import org.springframework.expression.spel.ast.OpOr;
import org.springframework.expression.spel.support.StandardEvaluationContext;
@@ -34,6 +37,7 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.springframework.expression.spel.SpelMessage.MISSING_CONSTRUCTOR_ARGS;
import static org.springframework.expression.spel.SpelMessage.NON_TERMINATING_DOUBLE_QUOTED_STRING;
import static org.springframework.expression.spel.SpelMessage.NON_TERMINATING_QUOTED_STRING;
@@ -56,6 +60,105 @@ class SpelParserTests {
private final SpelExpressionParser parser = new SpelExpressionParser();
@Nested // gh-36723
class MaxNestingDepthTests {
private final int maxNestingDepth = 10;
private final SpelExpressionParser parser = new SpelExpressionParser(configurationWithMaxNestingDepth(maxNestingDepth));
@Test
void maxNestingDepthWithInlineLists() {
// 9 < max
assertThatNoException().isThrownBy(() -> parser.parseExpression(nestedInlineList(9)));
// 10 <= max
assertThatNoException().isThrownBy(() -> parser.parseExpression(nestedInlineList(10)));
// 11 > max
assertNestingDepthExceeded(() -> parser.parseExpression(nestedInlineList(11)), maxNestingDepth);
// 100 > max
assertNestingDepthExceeded(() -> parser.parseExpression(nestedInlineList(100)), maxNestingDepth);
}
@Test
void maxNestingDepthWithParentheses() {
assertThatNoException().isThrownBy(() -> parser.parseExpression(nestedParentheses(9)));
assertThatNoException().isThrownBy(() -> parser.parseExpression(nestedParentheses(10)));
assertNestingDepthExceeded(() -> parser.parseExpression(nestedParentheses(11)), maxNestingDepth);
assertNestingDepthExceeded(() -> parser.parseExpression(nestedParentheses(100)), maxNestingDepth);
}
@Test
void maxNestingDepthWithChainedUnaryOperators() {
assertThatNoException().isThrownBy(() -> parser.parseExpression("!".repeat(9) + "true"));
assertThatNoException().isThrownBy(() -> parser.parseExpression("!".repeat(10) + "true"));
assertNestingDepthExceeded(() -> parser.parseExpression("!".repeat(11) + "true"), maxNestingDepth);
assertNestingDepthExceeded(() -> parser.parseExpression("-".repeat(100) + "1"), maxNestingDepth);
}
@Test
void maxNestingDepthProtectsAgainstStackOverflowFromChainedUnaryOperators() {
// Effectively disable the expression-length limit so that the nesting-depth
// limit is the guard that stops the parser well before the JVM call stack does.
SpelParserConfiguration configuration = new SpelParserConfiguration(SpelCompilerMode.OFF, null, false, false,
0, Integer.MAX_VALUE, SpelParserConfiguration.DEFAULT_MAX_OPERATIONS,
SpelParserConfiguration.DEFAULT_MAX_BIG_POWER_BITS,
SpelParserConfiguration.DEFAULT_MAX_EXPRESSION_NESTING_DEPTH);
SpelExpressionParser parser = new SpelExpressionParser(configuration);
assertParseExceptionThrownBy(() -> parser.parseExpression("!".repeat(100_000) + "true"))
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.MAX_EXPRESSION_NESTING_DEPTH_EXCEEDED));
}
@Test
void maxNestingDepthIsNotExceededBySequentialNonNestedTernaryExpressions() {
// Many independent (sibling, non-nested) ternary expressions as elements of a
// single inline list should not accumulate nesting depth across elements.
String manySiblingTernaryExpressions = "{" + "(true ? 1 : 2),".repeat(50) + "(true ? 1 : 2)}";
assertThatNoException().isThrownBy(() -> parser.parseExpression(manySiblingTernaryExpressions));
}
@Test
void maxNestingDepthWithNestedTernaryExpressions() {
assertThatNoException().isThrownBy(() -> parser.parseExpression(nestedTernaryExpression(2)));
assertNestingDepthExceeded(() -> parser.parseExpression(nestedTernaryExpression(50)), maxNestingDepth);
}
private static SpelParserConfiguration configurationWithMaxNestingDepth(int maxNestingDepth) {
return new SpelParserConfiguration(SpelCompilerMode.OFF, null, false, false, 0, 10_000,
SpelParserConfiguration.DEFAULT_MAX_OPERATIONS, SpelParserConfiguration.DEFAULT_MAX_BIG_POWER_BITS,
maxNestingDepth);
}
private static void assertNestingDepthExceeded(ThrowingCallable throwingCallable, int maxNestingDepth) {
assertParseExceptionThrownBy(throwingCallable)
.withMessageEndingWith("SpEL expression nesting depth exceeds the threshold of " + maxNestingDepth)
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.MAX_EXPRESSION_NESTING_DEPTH_EXCEEDED));
}
private static String nestedInlineList(int depth) {
return "{".repeat(depth) + "1" + "}".repeat(depth);
}
private static String nestedParentheses(int depth) {
return "(".repeat(depth) + "1" + ")".repeat(depth);
}
private static String nestedTernaryExpression(int depth) {
String expression = "1";
for (int i = 0; i < depth; i++) {
expression = "true ? 1 : " + expression;
}
return expression;
}
}
@Test
void nullExpressionIsRejected() {
assertNullOrEmptyExpressionIsRejected(() -> parser.parseExpression(null));