Skip to content

Commit 26f2dad

Browse files
committed
Enforce read-only semantics in SpEL's SimpleEvaluationContext
SimpleEvaluationContext.forReadOnlyDataBinding() documents that it creates a SimpleEvaluationContext for read-only access to public properties; however, prior to this commit write access was not disabled for indexed structures when using the assignment operator, the increment operator, or the decrement operator. In order to better align with the documented contract for forReadOnlyDataBinding(), this commit makes it possible to disable assignment in general in order to enforce read-only semantics for SpEL's SimpleEvaluationContext when created via the forReadOnlyDataBinding() factory method. Specifically: - This commit introduces a new isAssignmentEnabled() "default" method in the EvaluationContext API, which returns true by default. - SimpleEvaluationContext overrides isAssignmentEnabled(), returning false if the context was created via the forReadOnlyDataBinding() factory method. - The Assign, OpDec, and OpInc AST nodes -- representing the assignment (=), increment (++), and decrement (--) operators, respectively -- now throw a SpelEvaluationException if assignment is disabled for the current EvaluationContext. See gh-33319 Closes gh-33320 (cherry picked from commit e1ab306)
1 parent 7e39078 commit 26f2dad

File tree

9 files changed

+687
-109
lines changed

9 files changed

+687
-109
lines changed

spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -132,4 +132,18 @@ default TypedValue assignVariable(String name, Supplier<TypedValue> valueSupplie
132132
@Nullable
133133
Object lookupVariable(String name);
134134

135+
/**
136+
* Determine if assignment is enabled within expressions evaluated by this evaluation
137+
* context.
138+
* <p>If this method returns {@code false}, the assignment ({@code =}), increment
139+
* ({@code ++}), and decrement ({@code --}) operators are disabled.
140+
* <p>By default, this method returns {@code true}. Concrete implementations may override
141+
* this <em>default</em> method to disable assignment.
142+
* @return {@code true} if assignment is enabled; {@code false} otherwise
143+
* @since 5.3.38
144+
*/
145+
default boolean isAssignmentEnabled() {
146+
return true;
147+
}
148+
135149
}

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,8 @@
1919
import org.springframework.expression.EvaluationException;
2020
import org.springframework.expression.TypedValue;
2121
import org.springframework.expression.spel.ExpressionState;
22+
import org.springframework.expression.spel.SpelEvaluationException;
23+
import org.springframework.expression.spel.SpelMessage;
2224

2325
/**
2426
* Represents assignment. An alternative to calling {@code setValue}
@@ -39,6 +41,9 @@ public Assign(int startPos, int endPos, SpelNodeImpl... operands) {
3941

4042
@Override
4143
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
44+
if (!state.getEvaluationContext().isAssignmentEnabled()) {
45+
throw new SpelEvaluationException(getStartPosition(), SpelMessage.NOT_ASSIGNABLE, toStringAST());
46+
}
4247
return this.children[0].setValueInternal(state, () -> this.children[1].getValueInternal(state));
4348
}
4449

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -34,6 +34,7 @@
3434
* @author Andy Clement
3535
* @author Juergen Hoeller
3636
* @author Giovanni Dall'Oglio Risso
37+
* @author Sam Brannen
3738
* @since 3.2
3839
*/
3940
public class OpDec extends Operator {
@@ -50,6 +51,10 @@ public OpDec(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands
5051

5152
@Override
5253
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
54+
if (!state.getEvaluationContext().isAssignmentEnabled()) {
55+
throw new SpelEvaluationException(getStartPosition(), SpelMessage.OPERAND_NOT_DECREMENTABLE, toStringAST());
56+
}
57+
5358
SpelNodeImpl operand = getLeftOperand();
5459

5560
// The operand is going to be read and then assigned to, we don't want to evaluate it twice.

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -34,6 +34,7 @@
3434
* @author Andy Clement
3535
* @author Juergen Hoeller
3636
* @author Giovanni Dall'Oglio Risso
37+
* @author Sam Brannen
3738
* @since 3.2
3839
*/
3940
public class OpInc extends Operator {
@@ -50,6 +51,10 @@ public OpInc(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands
5051

5152
@Override
5253
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
54+
if (!state.getEvaluationContext().isAssignmentEnabled()) {
55+
throw new SpelEvaluationException(getStartPosition(), SpelMessage.OPERAND_NOT_INCREMENTABLE, toStringAST());
56+
}
57+
5358
SpelNodeImpl operand = getLeftOperand();
5459
ValueRef valueRef = operand.getValueRef(state);
5560

@@ -104,7 +109,7 @@ else if (op1 instanceof Byte) {
104109
}
105110
}
106111

107-
// set the name value
112+
// set the new value
108113
try {
109114
valueRef.setValue(newValue.getValue());
110115
}

spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java

+56-23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -51,25 +51,25 @@
5151
* SpEL language syntax, e.g. excluding references to Java types, constructors,
5252
* and bean references.
5353
*
54-
* <p>When creating a {@code SimpleEvaluationContext} you need to choose the
55-
* level of support that you need for property access in SpEL expressions:
54+
* <p>When creating a {@code SimpleEvaluationContext} you need to choose the level of
55+
* support that you need for data binding in SpEL expressions:
5656
* <ul>
57-
* <li>A custom {@code PropertyAccessor} (typically not reflection-based),
58-
* potentially combined with a {@link DataBindingPropertyAccessor}</li>
59-
* <li>Data binding properties for read-only access</li>
60-
* <li>Data binding properties for read and write</li>
57+
* <li>Data binding for read-only access</li>
58+
* <li>Data binding for read and write access</li>
59+
* <li>A custom {@code PropertyAccessor} (typically not reflection-based), potentially
60+
* combined with a {@link DataBindingPropertyAccessor}</li>
6161
* </ul>
6262
*
63-
* <p>Conveniently, {@link SimpleEvaluationContext#forReadOnlyDataBinding()}
64-
* enables read access to properties via {@link DataBindingPropertyAccessor};
65-
* same for {@link SimpleEvaluationContext#forReadWriteDataBinding()} when
66-
* write access is needed as well. Alternatively, configure custom accessors
67-
* via {@link SimpleEvaluationContext#forPropertyAccessors}, and potentially
68-
* activate method resolution and/or a type converter through the builder.
63+
* <p>Conveniently, {@link SimpleEvaluationContext#forReadOnlyDataBinding()} enables
64+
* read-only access to properties via {@link DataBindingPropertyAccessor}. Similarly,
65+
* {@link SimpleEvaluationContext#forReadWriteDataBinding()} enables read and write access
66+
* to properties. Alternatively, configure custom accessors via
67+
* {@link SimpleEvaluationContext#forPropertyAccessors} and potentially activate method
68+
* resolution and/or a type converter through the builder.
6969
*
7070
* <p>Note that {@code SimpleEvaluationContext} is typically not configured
7171
* with a default root object. Instead it is meant to be created once and
72-
* used repeatedly through {@code getValue} calls on a pre-compiled
72+
* used repeatedly through {@code getValue} calls on a predefined
7373
* {@link org.springframework.expression.Expression} with both an
7474
* {@code EvaluationContext} and a root object as arguments:
7575
* {@link org.springframework.expression.Expression#getValue(EvaluationContext, Object)}.
@@ -81,9 +81,9 @@
8181
* @author Juergen Hoeller
8282
* @author Sam Brannen
8383
* @since 4.3.15
84-
* @see #forPropertyAccessors
8584
* @see #forReadOnlyDataBinding()
8685
* @see #forReadWriteDataBinding()
86+
* @see #forPropertyAccessors
8787
* @see StandardEvaluationContext
8888
* @see StandardTypeConverter
8989
* @see DataBindingPropertyAccessor
@@ -109,14 +109,17 @@ public final class SimpleEvaluationContext implements EvaluationContext {
109109

110110
private final Map<String, Object> variables = new HashMap<>();
111111

112+
private final boolean assignmentEnabled;
113+
112114

113115
private SimpleEvaluationContext(List<PropertyAccessor> accessors, List<MethodResolver> resolvers,
114-
@Nullable TypeConverter converter, @Nullable TypedValue rootObject) {
116+
@Nullable TypeConverter converter, @Nullable TypedValue rootObject, boolean assignmentEnabled) {
115117

116118
this.propertyAccessors = accessors;
117119
this.methodResolvers = resolvers;
118120
this.typeConverter = (converter != null ? converter : new StandardTypeConverter());
119121
this.rootObject = (rootObject != null ? rootObject : TypedValue.NULL);
122+
this.assignmentEnabled = assignmentEnabled;
120123
}
121124

122125

@@ -224,15 +227,33 @@ public Object lookupVariable(String name) {
224227
return this.variables.get(name);
225228
}
226229

230+
/**
231+
* Determine if assignment is enabled within expressions evaluated by this evaluation
232+
* context.
233+
* <p>If this method returns {@code false}, the assignment ({@code =}), increment
234+
* ({@code ++}), and decrement ({@code --}) operators are disabled.
235+
* @return {@code true} if assignment is enabled; {@code false} otherwise
236+
* @since 5.3.38
237+
* @see #forPropertyAccessors(PropertyAccessor...)
238+
* @see #forReadOnlyDataBinding()
239+
* @see #forReadWriteDataBinding()
240+
*/
241+
@Override
242+
public boolean isAssignmentEnabled() {
243+
return this.assignmentEnabled;
244+
}
227245

228246
/**
229247
* Create a {@code SimpleEvaluationContext} for the specified {@link PropertyAccessor}
230248
* delegates: typically a custom {@code PropertyAccessor} specific to a use case
231249
* (e.g. attribute resolution in a custom data structure), potentially combined with
232250
* a {@link DataBindingPropertyAccessor} if property dereferences are needed as well.
251+
* <p>Assignment is enabled within expressions evaluated by the context created via
252+
* this factory method.
233253
* @param accessors the accessor delegates to use
234254
* @see DataBindingPropertyAccessor#forReadOnlyAccess()
235255
* @see DataBindingPropertyAccessor#forReadWriteAccess()
256+
* @see #isAssignmentEnabled()
236257
*/
237258
public static Builder forPropertyAccessors(PropertyAccessor... accessors) {
238259
for (PropertyAccessor accessor : accessors) {
@@ -241,34 +262,40 @@ public static Builder forPropertyAccessors(PropertyAccessor... accessors) {
241262
"ReflectivePropertyAccessor. Consider using DataBindingPropertyAccessor or a custom subclass.");
242263
}
243264
}
244-
return new Builder(accessors);
265+
return new Builder(true, accessors);
245266
}
246267

247268
/**
248269
* Create a {@code SimpleEvaluationContext} for read-only access to
249270
* public properties via {@link DataBindingPropertyAccessor}.
271+
* <p>Assignment is disabled within expressions evaluated by the context created via
272+
* this factory method.
250273
* @see DataBindingPropertyAccessor#forReadOnlyAccess()
251274
* @see #forPropertyAccessors
275+
* @see #isAssignmentEnabled()
252276
*/
253277
public static Builder forReadOnlyDataBinding() {
254-
return new Builder(DataBindingPropertyAccessor.forReadOnlyAccess());
278+
return new Builder(false, DataBindingPropertyAccessor.forReadOnlyAccess());
255279
}
256280

257281
/**
258282
* Create a {@code SimpleEvaluationContext} for read-write access to
259283
* public properties via {@link DataBindingPropertyAccessor}.
284+
* <p>Assignment is enabled within expressions evaluated by the context created via
285+
* this factory method.
260286
* @see DataBindingPropertyAccessor#forReadWriteAccess()
261287
* @see #forPropertyAccessors
288+
* @see #isAssignmentEnabled()
262289
*/
263290
public static Builder forReadWriteDataBinding() {
264-
return new Builder(DataBindingPropertyAccessor.forReadWriteAccess());
291+
return new Builder(true, DataBindingPropertyAccessor.forReadWriteAccess());
265292
}
266293

267294

268295
/**
269296
* Builder for {@code SimpleEvaluationContext}.
270297
*/
271-
public static class Builder {
298+
public static final class Builder {
272299

273300
private final List<PropertyAccessor> accessors;
274301

@@ -280,10 +307,15 @@ public static class Builder {
280307
@Nullable
281308
private TypedValue rootObject;
282309

283-
public Builder(PropertyAccessor... accessors) {
310+
private final boolean assignmentEnabled;
311+
312+
313+
private Builder(boolean assignmentEnabled, PropertyAccessor... accessors) {
314+
this.assignmentEnabled = assignmentEnabled;
284315
this.accessors = Arrays.asList(accessors);
285316
}
286317

318+
287319
/**
288320
* Register the specified {@link MethodResolver} delegates for
289321
* a combination of property access and method resolution.
@@ -315,7 +347,6 @@ public Builder withInstanceMethods() {
315347
return this;
316348
}
317349

318-
319350
/**
320351
* Register a custom {@link ConversionService}.
321352
* <p>By default a {@link StandardTypeConverter} backed by a
@@ -327,6 +358,7 @@ public Builder withConversionService(ConversionService conversionService) {
327358
this.typeConverter = new StandardTypeConverter(conversionService);
328359
return this;
329360
}
361+
330362
/**
331363
* Register a custom {@link TypeConverter}.
332364
* <p>By default a {@link StandardTypeConverter} backed by a
@@ -362,7 +394,8 @@ public Builder withTypedRootObject(Object rootObject, TypeDescriptor typeDescrip
362394
}
363395

364396
public SimpleEvaluationContext build() {
365-
return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter, this.rootObject);
397+
return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter, this.rootObject,
398+
this.assignmentEnabled);
366399
}
367400
}
368401

0 commit comments

Comments
 (0)