Skip to content

Commit e1ab306

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-33321 (cherry picked from commit 0127de5)
1 parent 2165b16 commit e1ab306

File tree

9 files changed

+685
-109
lines changed

9 files changed

+685
-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

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 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,6 +51,10 @@ public OpDec(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands
5151

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

5660
// 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

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 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,6 +51,10 @@ public OpInc(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands
5151

5252
@Override
5353
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
54+
if (!state.getEvaluationContext().isAssignmentEnabled()) {
55+
throw new SpelEvaluationException(getStartPosition(), SpelMessage.OPERAND_NOT_INCREMENTABLE, toStringAST());
56+
}
57+
5458
SpelNodeImpl operand = getLeftOperand();
5559
ValueRef valueRef = operand.getValueRef(state);
5660

@@ -104,7 +108,7 @@ else if (op1 instanceof Byte) {
104108
}
105109
}
106110

107-
// set the name value
111+
// set the new value
108112
try {
109113
valueRef.setValue(newValue.getValue());
110114
}

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

+55-22
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.
@@ -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

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.expression.spel;
18+
19+
import java.util.Map;
20+
21+
import org.springframework.asm.MethodVisitor;
22+
import org.springframework.expression.AccessException;
23+
import org.springframework.expression.EvaluationContext;
24+
import org.springframework.expression.TypedValue;
25+
import org.springframework.lang.Nullable;
26+
import org.springframework.util.Assert;
27+
28+
/**
29+
* This is a local COPY of {@link org.springframework.context.expression.MapAccessor}.
30+
*
31+
* @author Juergen Hoeller
32+
* @author Andy Clement
33+
* @since 4.1
34+
*/
35+
public class CompilableMapAccessor implements CompilablePropertyAccessor {
36+
37+
@Override
38+
public Class<?>[] getSpecificTargetClasses() {
39+
return new Class<?>[] {Map.class};
40+
}
41+
42+
@Override
43+
public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
44+
return (target instanceof Map<?, ?> map && map.containsKey(name));
45+
}
46+
47+
@Override
48+
public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
49+
Assert.state(target instanceof Map, "Target must be of type Map");
50+
Map<?, ?> map = (Map<?, ?>) target;
51+
Object value = map.get(name);
52+
if (value == null && !map.containsKey(name)) {
53+
throw new MapAccessException(name);
54+
}
55+
return new TypedValue(value);
56+
}
57+
58+
@Override
59+
public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
60+
return true;
61+
}
62+
63+
@Override
64+
@SuppressWarnings("unchecked")
65+
public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue)
66+
throws AccessException {
67+
68+
Assert.state(target instanceof Map, "Target must be a Map");
69+
Map<Object, Object> map = (Map<Object, Object>) target;
70+
map.put(name, newValue);
71+
}
72+
73+
@Override
74+
public boolean isCompilable() {
75+
return true;
76+
}
77+
78+
@Override
79+
public Class<?> getPropertyType() {
80+
return Object.class;
81+
}
82+
83+
@Override
84+
public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) {
85+
String descriptor = cf.lastDescriptor();
86+
if (descriptor == null || !descriptor.equals("Ljava/util/Map")) {
87+
if (descriptor == null) {
88+
cf.loadTarget(mv);
89+
}
90+
CodeFlow.insertCheckCast(mv, "Ljava/util/Map");
91+
}
92+
mv.visitLdcInsn(propertyName);
93+
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get","(Ljava/lang/Object;)Ljava/lang/Object;",true);
94+
}
95+
96+
97+
/**
98+
* Exception thrown from {@code read} in order to reset a cached
99+
* PropertyAccessor, allowing other accessors to have a try.
100+
*/
101+
@SuppressWarnings("serial")
102+
private static class MapAccessException extends AccessException {
103+
104+
private final String key;
105+
106+
public MapAccessException(String key) {
107+
super("");
108+
this.key = key;
109+
}
110+
111+
@Override
112+
public String getMessage() {
113+
return "Map does not contain a value for key '" + this.key + "'";
114+
}
115+
}
116+
117+
}

0 commit comments

Comments
 (0)