Skip to content

Add configurable structural nesting depth limit to SpEL expressions#36960

Closed
itsmehotpants wants to merge 1 commit into
spring-projects:mainfrom
itsmehotpants:feature/spel-max-nesting-depth
Closed

Add configurable structural nesting depth limit to SpEL expressions#36960
itsmehotpants wants to merge 1 commit into
spring-projects:mainfrom
itsmehotpants:feature/spel-max-nesting-depth

Conversation

@itsmehotpants

Copy link
Copy Markdown

What this PR does

Closes #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 {'a': {'b': {'c': …}}}. Without a bound, a crafted expression can drive the recursive-descent parser into an arbitrarily deep Java call stack, eventually causing StackOverflowError.

This PR adds a configurable maximumNestingDepth limit that fires a descriptive SpelParseException the moment the parser descends past the threshold.


Changes

SpelMessage

  • New entry MAX_NESTING_DEPTH_EXCEEDED (code 1086) used when the configured limit is exceeded during parsing.

SpelParserConfiguration

  • DEFAULT_MAX_NESTING_DEPTH = 1_000 — conservative default that is safe for any legitimate expression.
  • SPRING_EXPRESSION_MAX_NESTING_DEPTH_PROPERTY_NAME (spring.expression.maxNestingDepth) — system property / SpringProperties key for global override, consistent with the existing maxOperations pattern.
  • maximumNestingDepth field + getMaximumNestingDepth() accessor.
  • New 8-arg canonical constructor that adds maximumNestingDepth; the previous 7-arg constructor delegates to it and is marked @Deprecated(since = "7.0").
  • retrieveMaxNestingDepth() static helper follows exactly the same validation pattern as retrieveMaxOperations().

InternalSpelExpressionParser

  • nestingDepth counter field, reset to 0 at the start of every doParseExpression call (ensuring parser instances are safely reusable).
  • enterNesting(startPos) — increments the counter and throws via internalException if the limit is exceeded.
  • exitNesting() — decrements the counter.
  • maybeEatInlineListOrMap() — calls enterNesting() on LCURLY and exitNesting() just before the finished node is pushed, so every inline list and map contributes exactly one depth level.

SpelNestingDepthTests (new)

15 JUnit 5 tests covering:

  • Configuration defaults and validation
  • Inline lists: flat, at-limit, over-limit, deeply nested past default
  • Inline maps: flat, at-limit, over-limit
  • Mixed list + map nesting
  • Limit boundary conditions
  • Parser reusability after an exception
  • Error message containing the configured limit value

Usage

// Global override via system property:
System.setProperty("spring.expression.maxNestingDepth", "50");

// Per-parser override via constructor:
SpelParserConfiguration config = new SpelParserConfiguration(
        SpelCompilerMode.OFF, null,
        false, false, Integer.MAX_VALUE,
        SpelParserConfiguration.DEFAULT_MAX_EXPRESSION_LENGTH,
        SpelParserConfiguration.DEFAULT_MAX_OPERATIONS,
        50 // maximumNestingDepth
);
SpelExpressionParser parser = new SpelExpressionParser(config);

// Throws SpelParseException (EL1086E) for {{{{…}}}}}:
parser.parseExpression("{".repeat(51) + "}".repeat(51));

Checklist

  • Implements the feature requested in Add a configurable limit for structural nesting depth in SpEL expressions #36723
  • Follows existing limit patterns (maximumExpressionLength, maximumOperations)
  • System property support (spring.expression.maxNestingDepth)
  • @Deprecated(since = "7.0") on the superseded 7-arg constructor
  • 15 unit tests with boundary, reuse, and error-message assertions
  • No new runtime dependencies

Closes spring-projects#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.
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jun 23, 2026
@sbrannen

Copy link
Copy Markdown
Member

Hi @itsmehotpants,

Congratulations on submitting your first PR for the Spring Framework! 👍

Unfortunately, #36723 is assigned to me and not open to contributions.

I stated this already in #36723 (comment).

In the future, please make sure you read all comments in an issue before spending time crafting and submitting a PR.

Regards,

Sam

@sbrannen sbrannen closed this Jun 23, 2026
@sbrannen sbrannen added status: declined A suggestion or change that we don't feel we should currently apply in: core Issues in core modules (aop, beans, core, context, expression) and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

in: core Issues in core modules (aop, beans, core, context, expression) status: declined A suggestion or change that we don't feel we should currently apply

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a configurable limit for structural nesting depth in SpEL expressions

3 participants