mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-25 16:29:28 +00:00
Track operations during SpEL expression evaluation
This commit introduces support for tracking operations during SpEL expression evaluation. If the maximum number of operations is exceeded, a SpelEvaluationException is thrown. The limit can be configured either on a per-use-case basis via SpelParserConfiguration supplied to the SpelExpressionParser or globally as a JVM system property or Spring property named `spring.expression.maxOperations`. Closes gh-36801
This commit is contained in:
committed by
Brian Clozel
parent
83667f808c
commit
2c18c33ce0
+92
@@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import org.springframework.core.SpringProperties;
|
||||
import org.springframework.expression.AccessException;
|
||||
import org.springframework.expression.BeanResolver;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
@@ -47,6 +48,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.assertj.core.api.Assertions.within;
|
||||
import static org.springframework.expression.spel.SpelMessage.BETWEEN_RIGHT_OPERAND_MUST_BE_TWO_ELEMENT_LIST;
|
||||
import static org.springframework.expression.spel.SpelParserConfiguration.SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME;
|
||||
|
||||
/**
|
||||
* Tests the evaluation of real expressions in a real context.
|
||||
@@ -97,6 +99,96 @@ class EvaluationTests extends AbstractExpressionTests {
|
||||
evaluateAndCheckError(parser, expression, String.class, SpelMessage.MAX_EXPRESSION_LENGTH_EXCEEDED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxOperationsIsConfigurableViaSpelParserConfiguration() {
|
||||
int maxLength = 100;
|
||||
int maxOperations = 10;
|
||||
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
// The following expression contains 10 tracked operations:
|
||||
String expression = "('foo' + 'bar').length >= 6 && {1, 1 + 1, 3}.contains(3)";
|
||||
Expression expr1 = parser.parseExpression(expression);
|
||||
assertThat(expr1.getValue(Boolean.class)).isTrue();
|
||||
|
||||
SpelParserConfiguration configuration =
|
||||
new SpelParserConfiguration(SpelCompilerMode.OFF, null, false, false, 0, maxLength, maxOperations);
|
||||
parser = new SpelExpressionParser(configuration);
|
||||
Expression expr2 = parser.parseExpression(expression);
|
||||
assertThatExceptionOfType(SpelEvaluationException.class)
|
||||
.isThrownBy(() -> expr2.getValue(Boolean.class))
|
||||
.satisfies(ex -> {
|
||||
assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.MAX_OPERATIONS_EXCEEDED);
|
||||
assertThat(ex.getInserts()).as("inserts").containsExactly(maxOperations);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxOperationsIsConfigurableViaSpringProperty() {
|
||||
int maxOperations = 10;
|
||||
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
// The following expression contains 10 tracked operations:
|
||||
String expression = "('foo' + 'bar').length >= 6 && {1, 1 + 1, 3}.contains(3)";
|
||||
Expression expr1 = parser.parseExpression(expression);
|
||||
assertThat(expr1.getValue(Boolean.class)).isTrue();
|
||||
|
||||
try {
|
||||
SpringProperties.setProperty(SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME, "" + maxOperations);
|
||||
|
||||
parser = new SpelExpressionParser();
|
||||
Expression expr2 = parser.parseExpression(expression);
|
||||
assertThatExceptionOfType(SpelEvaluationException.class)
|
||||
.isThrownBy(() -> expr2.getValue(Boolean.class))
|
||||
.satisfies(ex -> {
|
||||
assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.MAX_OPERATIONS_EXCEEDED);
|
||||
assertThat(ex.getInserts()).as("inserts").containsExactly(maxOperations);
|
||||
});
|
||||
}
|
||||
finally {
|
||||
SpringProperties.setProperty(SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxOperationsConfiguredViaSpelParserConfigurationOverridesSpringProperty() {
|
||||
int maxLength = 100;
|
||||
int maxOperations = 10;
|
||||
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
// The following expression contains 10 tracked operations:
|
||||
String expression = "('foo' + 'bar').length >= 6 && {1, 1 + 1, 3}.contains(3)";
|
||||
Expression expr1 = parser.parseExpression(expression);
|
||||
assertThat(expr1.getValue(Boolean.class)).isTrue();
|
||||
|
||||
try {
|
||||
SpringProperties.setProperty(SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME, "" + maxOperations / 2);
|
||||
|
||||
// maxOperations + 1 should override maxOperations / 2
|
||||
SpelParserConfiguration configuration =
|
||||
new SpelParserConfiguration(SpelCompilerMode.OFF, null, false, false, 0, maxLength, maxOperations + 1);
|
||||
parser = new SpelExpressionParser(configuration);
|
||||
expr1 = parser.parseExpression(expression);
|
||||
assertThat(expr1.getValue(Boolean.class)).isTrue();
|
||||
|
||||
configuration = new SpelParserConfiguration(SpelCompilerMode.OFF, null, false, false, 0, maxLength, maxOperations);
|
||||
parser = new SpelExpressionParser(configuration);
|
||||
Expression expr2 = parser.parseExpression(expression);
|
||||
assertThatExceptionOfType(SpelEvaluationException.class)
|
||||
.isThrownBy(() -> expr2.getValue(Boolean.class))
|
||||
.satisfies(ex -> {
|
||||
assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.MAX_OPERATIONS_EXCEEDED);
|
||||
// Should fail due to maxOperations, not maxOperations / 2
|
||||
assertThat(ex.getInserts()).as("inserts").containsExactly(maxOperations);
|
||||
});
|
||||
}
|
||||
finally {
|
||||
SpringProperties.setProperty(SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void createListsOnAttemptToIndexNull01() throws EvaluationException, ParseException {
|
||||
ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true));
|
||||
|
||||
+57
-17
@@ -30,6 +30,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link InlineList} and {@link InlineMap}.
|
||||
@@ -47,17 +48,35 @@ class InlineCollectionTests {
|
||||
class InlineListTests {
|
||||
|
||||
@Test
|
||||
void listIsCached() {
|
||||
InlineList list = parseList("{1, -2, 3, 4}");
|
||||
assertThat(list.isConstant()).isTrue();
|
||||
assertThat(list.getConstantValue()).isEqualTo(List.of(1, -2, 3, 4));
|
||||
@SuppressWarnings({ "rawtypes", "unchecked", "deprecation" })
|
||||
void getConstantValue() {
|
||||
InlineList inlineList = parseList("{1, -2, 3, 4}");
|
||||
assertThat(inlineList.isConstant()).isTrue();
|
||||
List list1 = inlineList.getConstantValue();
|
||||
List list2 = inlineList.getValue(expressionState(), List.class);
|
||||
assertThat(list1).containsExactly(1, -2, 3, 4).isSameAs(list2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
void constantListIsCached() {
|
||||
ExpressionState expressionState = expressionState();
|
||||
|
||||
InlineList inlineList = parseList("{1, -2, 3, 4}");
|
||||
assertThat(inlineList.isConstant()).isTrue();
|
||||
List list1 = inlineList.getValue(expressionState, List.class);
|
||||
List list2 = inlineList.getValue(expressionState, List.class);
|
||||
assertThat(list1).containsExactly(1, -2, 3, 4).isSameAs(list2);
|
||||
// Constant inline lists are immutable.
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class)
|
||||
.isThrownBy(() -> list1.add(999));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dynamicListIsNotCached() {
|
||||
InlineList list = parseList("{1, (5 - 3), 3, 4}");
|
||||
assertThat(list.isConstant()).isFalse();
|
||||
assertThat(list.getValue(null)).isEqualTo(List.of(1, 2, 3, 4));
|
||||
assertThat(list.getValue(expressionState())).isEqualTo(List.of(1, 2, 3, 4));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -110,11 +129,28 @@ class InlineCollectionTests {
|
||||
class InlineMapTests {
|
||||
|
||||
@Test
|
||||
void mapIsCached() {
|
||||
InlineMap map = parseMap("{1 : 2, 3 : 4}");
|
||||
assertThat(map.isConstant()).isTrue();
|
||||
Map<Integer, Integer> expected = Map.of(1, 2, 3, 4);
|
||||
assertThat(map.getValue(null)).isEqualTo(expected);
|
||||
@SuppressWarnings({ "rawtypes", "unchecked", "deprecation" })
|
||||
void getConstantValue() {
|
||||
InlineMap inlineMap = parseMap("{1 : 2, 3 : 4}");
|
||||
assertThat(inlineMap.isConstant()).isTrue();
|
||||
Map map1 = inlineMap.getConstantValue();
|
||||
Map map2 = inlineMap.getValue(expressionState(), Map.class);
|
||||
assertThat(map1).isEqualTo(Map.of(1, 2, 3, 4)).isSameAs(map2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
void constantMapIsCached() {
|
||||
ExpressionState expressionState = expressionState();
|
||||
|
||||
InlineMap inlineMap = parseMap("{1 : 2, 3 : 4}");
|
||||
assertThat(inlineMap.isConstant()).isTrue();
|
||||
Map map1 = inlineMap.getValue(expressionState, Map.class);
|
||||
Map map2 = inlineMap.getValue(expressionState, Map.class);
|
||||
assertThat(map1).isEqualTo(Map.of(1, 2, 3, 4)).isSameAs(map2);
|
||||
// Constant inline maps are immutable.
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class)
|
||||
.isThrownBy(() -> map1.put(99, "X"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -122,7 +158,7 @@ class InlineCollectionTests {
|
||||
InlineMap map = parseMap("{-1 : 2, (-2 - 1) : -4}");
|
||||
assertThat(map.isConstant()).isFalse();
|
||||
Map<Integer, Integer> expected = Map.of(-1, 2, -3, -4);
|
||||
assertThat(map.getValue(null)).isEqualTo(expected);
|
||||
assertThat(map.getValue(expressionState())).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -155,7 +191,7 @@ class InlineCollectionTests {
|
||||
InlineMap map = parseMap("{-1 : 2, -3 : 4}");
|
||||
assertThat(map.isConstant()).isTrue();
|
||||
Map<Integer, Integer> expected = Map.of(-1, 2, -3, 4);
|
||||
assertThat(map.getValue(null)).isEqualTo(expected);
|
||||
assertThat(map.getValue(expressionState())).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -163,7 +199,7 @@ class InlineCollectionTests {
|
||||
InlineMap map = parseMap("{1 : -2, 3 : -4}");
|
||||
assertThat(map.isConstant()).isTrue();
|
||||
Map<Integer, Integer> expected = Map.of(1, -2, 3, -4);
|
||||
assertThat(map.getValue(null)).isEqualTo(expected);
|
||||
assertThat(map.getValue(expressionState())).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -171,7 +207,7 @@ class InlineCollectionTests {
|
||||
InlineMap map = parseMap("{1L : -2L, 3L : -4L}");
|
||||
assertThat(map.isConstant()).isTrue();
|
||||
Map<Long, Long> expected = Map.of(1L, -2L, 3L, -4L);
|
||||
assertThat(map.getValue(null)).isEqualTo(expected);
|
||||
assertThat(map.getValue(expressionState())).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -179,7 +215,7 @@ class InlineCollectionTests {
|
||||
InlineMap map = parseMap("{-1.0f : -2.0f, -3.0f : -4.0f}");
|
||||
assertThat(map.isConstant()).isTrue();
|
||||
Map<Float, Float> expected = Map.of(-1.0f, -2.0f, -3.0f, -4.0f);
|
||||
assertThat(map.getValue(null)).isEqualTo(expected);
|
||||
assertThat(map.getValue(expressionState())).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -187,7 +223,7 @@ class InlineCollectionTests {
|
||||
InlineMap map = parseMap("{-1.0 : -2.0, -3.0 : -4.0}");
|
||||
assertThat(map.isConstant()).isTrue();
|
||||
Map<Double, Double> expected = Map.of(-1.0, -2.0, -3.0, -4.0);
|
||||
assertThat(map.getValue(null)).isEqualTo(expected);
|
||||
assertThat(map.getValue(expressionState())).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -195,7 +231,7 @@ class InlineCollectionTests {
|
||||
InlineMap map = parseMap("{-1 : -2, -3 : -4}");
|
||||
assertThat(map.isConstant()).isTrue();
|
||||
Map<Integer, Integer> expected = Map.of(-1, -2, -3, -4);
|
||||
assertThat(map.getValue(null)).isEqualTo(expected);
|
||||
assertThat(map.getValue(expressionState())).isEqualTo(expected);
|
||||
}
|
||||
|
||||
private InlineMap parseMap(String s) {
|
||||
@@ -210,6 +246,10 @@ class InlineCollectionTests {
|
||||
return (SpelExpression) parser.parseExpression(s);
|
||||
}
|
||||
|
||||
private static ExpressionState expressionState() {
|
||||
return new ExpressionState(new StandardEvaluationContext());
|
||||
}
|
||||
|
||||
|
||||
private static class NumberHolder {
|
||||
@SuppressWarnings("unused")
|
||||
|
||||
Reference in New Issue
Block a user