Limit result size of BigDecimal/BigInteger power operations in SpEL

This commit introduces a configurable limit on the estimated result size
of BigDecimal and BigInteger power operations within SpEL expressions.
The estimated result size in bits is computed as the product of the base
value's bit length and the exponent. If this limit is exceeded, a
SpelEvaluationException is thrown.

The limit defaults to 1,000,000 bits, which is approximately equivalent
to a decimal number with 300,000 digits, and can be configured either
on a per-use-case basis via the new maximumBigPowerBits constructor
argument in SpelParserConfiguration or globally as a JVM system
property or Spring property named `spring.expression.maxBigPowerBits`.
Parsers intended for trusted internal expressions may supply
Integer.MAX_VALUE to remove the limit entirely.

Closes ch-37034
This commit is contained in:
Sam Brannen
2026-08-14 09:19:58 +02:00
committed by Brian Clozel
parent ee1874ac52
commit 8a92c19e4d
8 changed files with 259 additions and 24 deletions
@@ -19,6 +19,7 @@ package org.springframework.expression.spel;
import java.util.Arrays;
import java.util.List;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -168,6 +169,23 @@ public abstract class AbstractExpressionTests {
evaluateAndCheckError(this.parser, expression, expectedReturnType, expectedMessage, otherProperties);
}
/**
* Evaluate the specified expression and ensure the expected message comes out.
* The message may have inserts and they will be checked if otherProperties is specified.
* The first entry in otherProperties should always be the position.
* @param evaluationContext the evaluation context to use
* @param expression the expression to evaluate
* @param expectedReturnType ask the expression return value to be of this type if possible
* ({@code null} indicates don't ask for conversion)
* @param expectedMessage the expected message
* @param otherProperties the expected inserts within the message
*/
protected void evaluateAndCheckError(EvaluationContext evaluationContext, String expression,
Class<?> expectedReturnType, SpelMessage expectedMessage, Object... otherProperties) {
evaluateAndCheckError(this.parser, evaluationContext, expression, expectedReturnType, expectedMessage, otherProperties);
}
/**
* Evaluate the specified expression and ensure the expected message comes out.
* The message may have inserts and they will be checked if otherProperties is specified.
@@ -182,14 +200,32 @@ public abstract class AbstractExpressionTests {
protected void evaluateAndCheckError(ExpressionParser parser, String expression, Class<?> expectedReturnType, SpelMessage expectedMessage,
Object... otherProperties) {
evaluateAndCheckError(parser, this.context, expression, expectedReturnType, expectedMessage, otherProperties);
}
/**
* Evaluate the specified expression and ensure the expected message comes out.
* The message may have inserts and they will be checked if otherProperties is specified.
* The first entry in otherProperties should always be the position.
* @param parser the expression parser to use
* @param evaluationContext the evaluation context to use
* @param expression the expression to evaluate
* @param expectedReturnType ask the expression return value to be of this type if possible
* ({@code null} indicates don't ask for conversion)
* @param expectedMessage the expected message
* @param otherProperties the expected inserts within the message
*/
protected void evaluateAndCheckError(ExpressionParser parser, EvaluationContext evaluationContext,
String expression, Class<?> expectedReturnType, SpelMessage expectedMessage, Object... otherProperties) {
assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> {
Expression expr = parser.parseExpression(expression);
assertThat(expr).as("expression").isNotNull();
if (expectedReturnType != null) {
expr.getValue(context, expectedReturnType);
expr.getValue(evaluationContext, expectedReturnType);
}
else {
expr.getValue(context);
expr.getValue(evaluationContext);
}
}).satisfies(ex -> {
assertThat(ex.getMessageCode()).isEqualTo(expectedMessage);
@@ -811,6 +811,77 @@ class EvaluationTests extends AbstractExpressionTests {
}
@Nested
class PowerOperatorTests {
private final EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// Use a small limit (16 bits) to verify behavior in tests.
private static final int TEST_MAX_RESULT_BITS = 16;
private final SpelExpressionParser limitedParser = new SpelExpressionParser(
new SpelParserConfiguration(SpelCompilerMode.OFF, null, false, false,
0, 10, 10, TEST_MAX_RESULT_BITS));
@Test
void powerOperatorWithBigDecimal() {
context.setVariable("bd", BigDecimal.valueOf(2.0));
Expression expr = parser.parseExpression("#bd ^ 4");
assertThat(expr.getValue(context, BigDecimal.class)).isEqualByComparingTo("16");
}
@Test
void powerOperatorWithBigDecimalUnderResultLimit() {
// BigDecimal.valueOf(2.0).unscaledValue().bitLength() = 5
// 5 * 3 = 15 bits <= TEST_MAX_RESULT_BITS (16)
context.setVariable("bd", BigDecimal.valueOf(2.0));
Expression expr = limitedParser.parseExpression("#bd ^ 3");
assertThat(expr.getValue(context, BigDecimal.class)).isEqualByComparingTo("8");
}
@Test
void powerOperatorWithBigDecimalExceedingResultLimit() {
// 5 * 4 = 20 bits > TEST_MAX_RESULT_BITS (16)
context.setVariable("bd", BigDecimal.valueOf(2.0));
evaluateAndCheckError(limitedParser, context, "#bd ^ 4", BigDecimal.class,
SpelMessage.MAX_BIG_POWER_RESULT_EXCEEDED,
4, // power operator position
5, // base bit length
4, // exponent
TEST_MAX_RESULT_BITS);
}
@Test
void powerOperatorWithBigInteger() {
context.setVariable("bi", BigInteger.valueOf(2));
Expression expr = parser.parseExpression("#bi ^ 4");
assertThat(expr.getValue(context, BigInteger.class)).isEqualTo(BigInteger.valueOf(16));
}
@Test
void powerOperatorWithBigIntegerUnderResultLimit() {
// BigInteger.valueOf(2).bitLength() = 2
// 2 * 8 = 16 bits == TEST_MAX_RESULT_BITS (16)
context.setVariable("bi", BigInteger.valueOf(2));
Expression expr = limitedParser.parseExpression("#bi ^ 8");
assertThat(expr.getValue(context, BigInteger.class)).isEqualTo(BigInteger.valueOf(256));
}
@Test
void powerOperatorWithBigIntegerExceedingResultLimit() {
// 2 * 9 = 18 bits > TEST_MAX_RESULT_BITS (16)
context.setVariable("bi", BigInteger.valueOf(2));
evaluateAndCheckError(limitedParser, context, "#bi ^ 9", BigInteger.class,
SpelMessage.MAX_BIG_POWER_RESULT_EXCEEDED,
4, // power operator position
2, // base bit length
9, // exponent
TEST_MAX_RESULT_BITS);
}
}
@Nested
class TernaryOperatorTests {