diff --git a/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc b/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc index ef3668351bb..ca5d2b830d0 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc @@ -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 <> discussed previously in this chapter. +==== + [[expressions-spel-compilation]] == SpEL Compilation 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 28cea823c73..9d656a1e1c3 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 @@ -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; 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 73e218ec5f8..ae8ef042567 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 @@ -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}. + *

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. + *

NOTE: 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 not 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 + * Security Considerations 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}. *

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)} + * {@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. *

Can also be configured via the {@link SpringProperties} mechanism. @@ -75,7 +99,7 @@ public class SpelParserConfiguration { * during SpEL expression evaluation: {@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)} + * {@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. *

Can also be configured via the {@link SpringProperties} mechanism. @@ -90,7 +114,7 @@ public class SpelParserConfiguration { * operation within a SpEL expression: {@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)} + * {@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. *

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. *

NOTE: 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. *

NOTE: 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. *

NOTE: 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. *

NOTE: 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. *

NOTE: 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. *

NOTE: 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. *

NOTE: 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. + *

NOTE: 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); 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 c94bea7277e..6d44912f8bc 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,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 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)); } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java index 230eb390e45..e1451bbd3f3 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java @@ -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));