Skip to content

Commit 76a36df

Browse files
sbrannenbclozel
authored andcommitted
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
1 parent 3d47da9 commit 76a36df

42 files changed

Lines changed: 615 additions & 209 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

framework-docs/modules/ROOT/pages/appendix.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ expressions used in XML bean definitions, `@Value`, etc.
7474
| The mode to use when compiling expressions for the
7575
xref:core/expressions/evaluation.adoc#expressions-compiler-configuration[Spring Expression Language].
7676

77+
| `spring.expression.maxOperations`
78+
| The default maximum number of operations permitted during
79+
xref:core/expressions/evaluation.adoc#expressions-parser-configuration[Spring Expression Language]
80+
expression evaluation.
81+
7782
| `spring.getenv.ignore`
7883
| Instructs Spring to ignore operating system environment variables if a Spring
7984
`Environment` property -- for example, a placeholder in a configuration String -- isn't

framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,16 @@ set a JVM system property or Spring property named `spring.context.expression.ma
395395
to the maximum expression length needed by your application (see
396396
xref:appendix.adoc#appendix-spring-properties[Supported Spring Properties]).
397397

398+
Similarly, the number of operations performed during the evaluation of a SpEL expression
399+
cannot exceed 10,000 by default; however, the `maxOperations` value is configurable. If
400+
you create a `SpelExpressionParser` programmatically (the recommend approach), you can
401+
specify a custom `maxOperations` value when creating the `SpelParserConfiguration` that
402+
you provide to the `SpelExpressionParser`. If you are not able to configure an explicit
403+
value for `maxOperations` via `SpelParserConfiguration`, you can set a JVM system
404+
property or Spring property named `spring.expression.maxOperations` to the maximum number
405+
of operations required by your application (see
406+
xref:appendix.adoc#appendix-spring-properties[Supported Spring Properties]).
407+
398408

399409
[[expressions-spel-compilation]]
400410
== SpEL Compilation

spring-core/src/main/java/org/springframework/core/SpringProperties.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
* @see org.springframework.beans.factory.support.DefaultListableBeanFactory#STRICT_LOCKING_PROPERTY_NAME
4545
* @see org.springframework.core.env.AbstractEnvironment#IGNORE_GETENV_PROPERTY_NAME
4646
* @see org.springframework.expression.spel.SpelParserConfiguration#SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
47+
* @see org.springframework.expression.spel.SpelParserConfiguration#SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
4748
* @see org.springframework.jdbc.core.StatementCreatorUtils#IGNORE_GETPARAMETERTYPE_PROPERTY_NAME
4849
* @see org.springframework.jndi.JndiLocatorDelegate#IGNORE_JNDI_PROPERTY_NAME
4950
* @see org.springframework.objenesis.SpringObjenesis#IGNORE_OBJENESIS_PROPERTY_NAME

spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ public class ExpressionState {
6767
@Nullable
6868
private Deque<VariableScope> variableScopes;
6969

70+
private int operationsCount;
71+
7072
// When entering a new scope there is a new base object which should be used
7173
// for '#this' references (or to act as a target for unqualified references).
7274
// This ArrayDeque captures those objects at each nested scope level.
@@ -146,6 +148,7 @@ public TypedValue getScopeRootContextObject() {
146148
* @see EvaluationContext#assignVariable(String, Supplier)
147149
*/
148150
public TypedValue assignVariable(String name, Supplier<TypedValue> valueSupplier) {
151+
trackOperation();
149152
return this.relatedContext.assignVariable(name, valueSupplier);
150153
}
151154

@@ -170,6 +173,7 @@ public void setVariable(String name, @Nullable Object value) {
170173
* @see #setVariable(String, Object)
171174
*/
172175
public TypedValue lookupVariable(String name) {
176+
trackOperation();
173177
Object value = this.relatedContext.lookupVariable(name);
174178
return (value != null ? new TypedValue(value) : TypedValue.NULL);
175179
}
@@ -301,6 +305,7 @@ private Deque<VariableScope> initVariableScopes() {
301305
public TypedValue operate(Operation op, @Nullable Object left, @Nullable Object right) throws EvaluationException {
302306
OperatorOverloader overloader = this.relatedContext.getOperatorOverloader();
303307
if (overloader.overridesOperation(op, left, right)) {
308+
trackOperation();
304309
Object returnValue = overloader.operate(op, left, right);
305310
return new TypedValue(returnValue);
306311
}
@@ -323,6 +328,18 @@ public SpelParserConfiguration getConfiguration() {
323328
return this.configuration;
324329
}
325330

331+
/**
332+
* Track an operation during expression evaluation.
333+
* @since 6.2.19
334+
* @see SpelParserConfiguration#getMaximumOperations()
335+
*/
336+
public void trackOperation() {
337+
int maxOperations = this.configuration.getMaximumOperations();
338+
if (++this.operationsCount >= maxOperations) {
339+
throw new SpelEvaluationException(SpelMessage.MAX_OPERATIONS_EXCEEDED, maxOperations);
340+
}
341+
}
342+
326343

327344
/**
328345
* A new local variable scope is entered when a new expression scope is

spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,11 @@ public enum SpelMessage {
303303

304304
/** @since 6.2 */
305305
EXCEPTION_DURING_INDEX_WRITE(Kind.ERROR, 1084,
306-
"A problem occurred while attempting to write index ''{0}'' in ''{1}''");
306+
"A problem occurred while attempting to write index ''{0}'' in ''{1}''"),
307307

308+
/** @since 6.2.19 */
309+
MAX_OPERATIONS_EXCEEDED(Kind.ERROR, 1085,
310+
"SpEL expression evaluation exceeded the threshold of ''{0}'' operations");
308311

309312

310313
private final Kind kind;

spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java

Lines changed: 144 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import org.springframework.core.SpringProperties;
2222
import org.springframework.lang.Nullable;
23+
import org.springframework.util.Assert;
24+
import org.springframework.util.StringUtils;
2325

2426
/**
2527
* Configuration object for the SpEL expression parser.
@@ -39,9 +41,38 @@ public class SpelParserConfiguration {
3941
*/
4042
public static final int DEFAULT_MAX_EXPRESSION_LENGTH = 10_000;
4143

42-
/** System property to configure the default compiler mode for SpEL expression parsers: {@value}. */
44+
/**
45+
* Default maximum number of operations permitted during SpEL expression evaluation: {@value}.
46+
* @since 6.2.19
47+
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
48+
*/
49+
public static final int DEFAULT_MAX_OPERATIONS = 10_000;
50+
51+
/**
52+
* System property to configure the default compiler mode for SpEL expression parsers: {@value}.
53+
* <p><strong>NOTE</strong>: Instead of relying on a global default, applications
54+
* and frameworks should ideally set an explicit custom value via the
55+
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
56+
* constructor which provides complete configuration control and the ability
57+
* to override global defaults per use case.
58+
* <p>Can also be configured via the {@link SpringProperties} mechanism.
59+
*/
4360
public static final String SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME = "spring.expression.compiler.mode";
4461

62+
/**
63+
* System property to configure the default maximum number of operations permitted
64+
* during SpEL expression evaluation: {@value}.
65+
* <p><strong>NOTE</strong>: Instead of relying on a global default, applications
66+
* and frameworks should ideally set an explicit custom value via the
67+
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
68+
* constructor which provides complete configuration control and the ability
69+
* to override global defaults per use case.
70+
* <p>Can also be configured via the {@link SpringProperties} mechanism.
71+
* @since 6.2.19
72+
* @see #DEFAULT_MAX_OPERATIONS
73+
*/
74+
public static final String SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME = "spring.expression.maxOperations";
75+
4576

4677
private static final SpelCompilerMode defaultCompilerMode;
4778

@@ -65,50 +96,85 @@ public class SpelParserConfiguration {
6596

6697
private final int maximumExpressionLength;
6798

99+
private final int maximumOperations;
100+
68101

69102
/**
70103
* Create a new {@code SpelParserConfiguration} instance with default settings.
104+
* <p><strong>NOTE</strong>: Favor the
105+
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
106+
* constructor for complete configuration control and the ability to override
107+
* global defaults per use case.
108+
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
109+
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
71110
*/
72111
public SpelParserConfiguration() {
73112
this(null, null, false, false, Integer.MAX_VALUE);
74113
}
75114

76115
/**
77116
* Create a new {@code SpelParserConfiguration} instance.
78-
* @param compilerMode the compiler mode for the parser
79-
* @param compilerClassLoader the ClassLoader to use as the basis for expression compilation
117+
* <p><strong>NOTE</strong>: Favor the
118+
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
119+
* constructor for complete configuration control and the ability to override
120+
* global defaults per use case.
121+
* @param compilerMode the compiler mode that parsers using this configuration
122+
* should use; or {@code null} to use the default mode
123+
* @param compilerClassLoader the {@code ClassLoader} to use as the basis for
124+
* expression compilation; or {@code null} to use the default {@code ClassLoader}
125+
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
126+
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
80127
*/
81128
public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader) {
82129
this(compilerMode, compilerClassLoader, false, false, Integer.MAX_VALUE);
83130
}
84131

85132
/**
86133
* Create a new {@code SpelParserConfiguration} instance.
134+
* <p><strong>NOTE</strong>: Favor the
135+
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
136+
* constructor for complete configuration control and the ability to override
137+
* global defaults per use case.
87138
* @param autoGrowNullReferences if null references should automatically grow
88139
* @param autoGrowCollections if collections should automatically grow
89-
* @see #SpelParserConfiguration(boolean, boolean, int)
140+
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
141+
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
90142
*/
91143
public SpelParserConfiguration(boolean autoGrowNullReferences, boolean autoGrowCollections) {
92144
this(null, null, autoGrowNullReferences, autoGrowCollections, Integer.MAX_VALUE);
93145
}
94146

95147
/**
96148
* Create a new {@code SpelParserConfiguration} instance.
149+
* <p><strong>NOTE</strong>: Favor the
150+
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
151+
* constructor for complete configuration control and the ability to override
152+
* global defaults per use case.
97153
* @param autoGrowNullReferences if null references should automatically grow
98154
* @param autoGrowCollections if collections should automatically grow
99-
* @param maximumAutoGrowSize the maximum size that the collection can auto grow
155+
* @param maximumAutoGrowSize the maximum size to which a collection can auto grow
156+
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
157+
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
100158
*/
101159
public SpelParserConfiguration(boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize) {
102160
this(null, null, autoGrowNullReferences, autoGrowCollections, maximumAutoGrowSize);
103161
}
104162

105163
/**
106164
* Create a new {@code SpelParserConfiguration} instance.
107-
* @param compilerMode the compiler mode that parsers using this configuration object should use
108-
* @param compilerClassLoader the ClassLoader to use as the basis for expression compilation
165+
* <p><strong>NOTE</strong>: Favor the
166+
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
167+
* constructor for complete configuration control and the ability to override
168+
* global defaults per use case.
169+
* @param compilerMode the compiler mode that parsers using this configuration
170+
* should use; or {@code null} to use the default mode
171+
* @param compilerClassLoader the {@code ClassLoader} to use as the basis for
172+
* expression compilation; or {@code null} to use the default {@code ClassLoader}
109173
* @param autoGrowNullReferences if null references should automatically grow
110174
* @param autoGrowCollections if collections should automatically grow
111-
* @param maximumAutoGrowSize the maximum size that the collection can auto grow
175+
* @param maximumAutoGrowSize the maximum size to which a collection can auto grow
176+
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
177+
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
112178
*/
113179
public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
114180
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize) {
@@ -119,24 +185,60 @@ public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullabl
119185

120186
/**
121187
* Create a new {@code SpelParserConfiguration} instance.
122-
* @param compilerMode the compiler mode that parsers using this configuration object should use
123-
* @param compilerClassLoader the ClassLoader to use as the basis for expression compilation
188+
* <p><strong>NOTE</strong>: Favor the
189+
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
190+
* constructor for complete configuration control and the ability to override
191+
* global defaults per use case.
192+
* @param compilerMode the compiler mode that parsers using this configuration
193+
* should use; or {@code null} to use the default mode
194+
* @param compilerClassLoader the {@code ClassLoader} to use as the basis for
195+
* expression compilation; or {@code null} to use the default {@code ClassLoader}
124196
* @param autoGrowNullReferences if null references should automatically grow
125197
* @param autoGrowCollections if collections should automatically grow
126-
* @param maximumAutoGrowSize the maximum size that a collection can auto grow
198+
* @param maximumAutoGrowSize the maximum size to which a collection can auto grow
127199
* @param maximumExpressionLength the maximum length of a SpEL expression;
128200
* must be a positive number
129201
* @since 5.2.25
202+
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
203+
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
130204
*/
131205
public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
132206
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize, int maximumExpressionLength) {
133207

134-
this.compilerMode = (compilerMode != null ? compilerMode : defaultCompilerMode);
208+
this((compilerMode != null ? compilerMode : defaultCompilerMode), compilerClassLoader, autoGrowNullReferences,
209+
autoGrowCollections, maximumAutoGrowSize, maximumExpressionLength, retrieveMaxOperations());
210+
}
211+
212+
/**
213+
* Create a new {@code SpelParserConfiguration} instance.
214+
* @param compilerMode the compiler mode that parsers using this configuration
215+
* should use; must not be {@code null}
216+
* @param compilerClassLoader the {@code ClassLoader} to use as the basis for
217+
* expression compilation; or {@code null} to use the default {@code ClassLoader}
218+
* @param autoGrowNullReferences if null references should automatically grow
219+
* @param autoGrowCollections if collections should automatically grow
220+
* @param maximumAutoGrowSize the maximum size to which a collection can auto grow
221+
* @param maximumExpressionLength the maximum length of a SpEL expression;
222+
* must be a positive number
223+
* @param maximumOperations the maximum number of operations permitted during
224+
* SpEL expression evaluation; must be a positive number
225+
* @since 6.2.19
226+
*/
227+
public SpelParserConfiguration(SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
228+
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize, int maximumExpressionLength,
229+
int maximumOperations) {
230+
231+
Assert.notNull(compilerMode, "'compilerMode' must not be null");
232+
Assert.isTrue(maximumExpressionLength > 0, "'maximumExpressionLength' must be a positive number");
233+
Assert.isTrue(maximumOperations > 0, "'maximumOperations' must be a positive number");
234+
235+
this.compilerMode = compilerMode;
135236
this.compilerClassLoader = compilerClassLoader;
136237
this.autoGrowNullReferences = autoGrowNullReferences;
137238
this.autoGrowCollections = autoGrowCollections;
138239
this.maximumAutoGrowSize = maximumAutoGrowSize;
139240
this.maximumExpressionLength = maximumExpressionLength;
241+
this.maximumOperations = maximumOperations;
140242
}
141243

142244

@@ -148,7 +250,7 @@ public SpelCompilerMode getCompilerMode() {
148250
}
149251

150252
/**
151-
* Return the ClassLoader to use as the basis for expression compilation.
253+
* Return the {@code ClassLoader} to use as the basis for expression compilation.
152254
*/
153255
@Nullable
154256
public ClassLoader getCompilerClassLoader() {
@@ -170,7 +272,7 @@ public boolean isAutoGrowCollections() {
170272
}
171273

172274
/**
173-
* Return the maximum size that a collection can auto grow.
275+
* Return the maximum size to which a collection can auto grow.
174276
*/
175277
public int getMaximumAutoGrowSize() {
176278
return this.maximumAutoGrowSize;
@@ -184,4 +286,32 @@ public int getMaximumExpressionLength() {
184286
return this.maximumExpressionLength;
185287
}
186288

289+
/**
290+
* Return the maximum number of operations permitted during SpEL expression
291+
* evaluation.
292+
* @since 6.2.19
293+
*/
294+
public int getMaximumOperations() {
295+
return this.maximumOperations;
296+
}
297+
298+
299+
private static int retrieveMaxOperations() {
300+
String value = SpringProperties.getProperty(SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME);
301+
if (!StringUtils.hasText(value)) {
302+
return DEFAULT_MAX_OPERATIONS;
303+
}
304+
305+
try {
306+
int maxOperations = Integer.parseInt(value.trim());
307+
Assert.isTrue(maxOperations > 0, () -> "Value [" + maxOperations + "] for system property [" +
308+
SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME + "] must be positive");
309+
return maxOperations;
310+
}
311+
catch (NumberFormatException ex) {
312+
throw new IllegalArgumentException("Failed to parse value for system property [" +
313+
SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME + "]: " + ex.getMessage(), ex);
314+
}
315+
}
316+
187317
}

spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep
4444
if (!state.getEvaluationContext().isAssignmentEnabled()) {
4545
throw new SpelEvaluationException(getStartPosition(), SpelMessage.NOT_ASSIGNABLE, toStringAST());
4646
}
47+
state.trackOperation();
4748
return this.children[0].setValueInternal(state, () -> this.children[1].getValueInternal(state));
4849
}
4950

spring-expression/src/main/java/org/springframework/expression/spel/ast/BeanReference.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep
6565
getStartPosition(), SpelMessage.NO_BEAN_RESOLVER_REGISTERED, this.beanName);
6666
}
6767

68+
state.trackOperation();
6869
try {
6970
return new TypedValue(beanResolver.resolve(state.getEvaluationContext(), this.beanName));
7071
}

spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ public ConstructorReference(int startPos, int endPos, SpelNodeImpl[] dimensions,
110110
*/
111111
@Override
112112
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
113+
state.trackOperation();
113114
if (this.isArrayConstructor) {
114115
return createArray(state);
115116
}

spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public Elvis(int startPos, int endPos, SpelNodeImpl... args) {
5151
*/
5252
@Override
5353
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
54+
state.trackOperation();
5455
TypedValue value = this.children[0].getValueInternal(state);
5556
// If this check is changed, the generateCode method will need changing too
5657
if (value.getValue() != null && !"".equals(value.getValue())) {

0 commit comments

Comments
 (0)