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); + } + +}