Skip to content

Commit 0127de5

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. Closes gh-33319
1 parent fcc99a6 commit 0127de5

File tree

8 files changed

+563
-27
lines changed

8 files changed

+563
-27
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.
@@ -133,4 +133,18 @@ default TypedValue assignVariable(String name, Supplier<TypedValue> valueSupplie
133133
@Nullable
134134
Object lookupVariable(String name);
135135

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

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

+4
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ public OpDec(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands
5353

5454
@Override
5555
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
56+
if (!state.getEvaluationContext().isAssignmentEnabled()) {
57+
throw new SpelEvaluationException(getStartPosition(), SpelMessage.OPERAND_NOT_DECREMENTABLE, toStringAST());
58+
}
59+
5660
SpelNodeImpl operand = getLeftOperand();
5761

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ public OpInc(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands
5353

5454
@Override
5555
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
56+
if (!state.getEvaluationContext().isAssignmentEnabled()) {
57+
throw new SpelEvaluationException(getStartPosition(), SpelMessage.OPERAND_NOT_INCREMENTABLE, toStringAST());
58+
}
59+
5660
SpelNodeImpl operand = getLeftOperand();
5761
ValueRef valueRef = operand.getValueRef(state);
5862

@@ -106,7 +110,7 @@ else if (op1 instanceof Byte) {
106110
}
107111
}
108112

109-
// set the name value
113+
// set the new value
110114
try {
111115
valueRef.setValue(newValue.getValue());
112116
}

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

+52-20
Original file line numberDiff line numberDiff line change
@@ -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)}.
@@ -89,9 +89,9 @@
8989
* @author Juergen Hoeller
9090
* @author Sam Brannen
9191
* @since 4.3.15
92-
* @see #forPropertyAccessors
9392
* @see #forReadOnlyDataBinding()
9493
* @see #forReadWriteDataBinding()
94+
* @see #forPropertyAccessors
9595
* @see StandardEvaluationContext
9696
* @see StandardTypeConverter
9797
* @see DataBindingPropertyAccessor
@@ -118,14 +118,18 @@ public final class SimpleEvaluationContext implements EvaluationContext {
118118

119119
private final Map<String, Object> variables = new HashMap<>();
120120

121+
private final boolean assignmentEnabled;
122+
123+
121124

122125
private SimpleEvaluationContext(List<PropertyAccessor> accessors, List<MethodResolver> resolvers,
123-
@Nullable TypeConverter converter, @Nullable TypedValue rootObject) {
126+
@Nullable TypeConverter converter, @Nullable TypedValue rootObject, boolean assignmentEnabled) {
124127

125128
this.propertyAccessors = accessors;
126129
this.methodResolvers = resolvers;
127130
this.typeConverter = (converter != null ? converter : new StandardTypeConverter());
128131
this.rootObject = (rootObject != null ? rootObject : TypedValue.NULL);
132+
this.assignmentEnabled = assignmentEnabled;
129133
}
130134

131135

@@ -253,15 +257,33 @@ public Object lookupVariable(String name) {
253257
return this.variables.get(name);
254258
}
255259

260+
/**
261+
* Determine if assignment is enabled within expressions evaluated by this evaluation
262+
* context.
263+
* <p>If this method returns {@code false}, the assignment ({@code =}), increment
264+
* ({@code ++}), and decrement ({@code --}) operators are disabled.
265+
* @return {@code true} if assignment is enabled; {@code false} otherwise
266+
* @since 5.3.38
267+
* @see #forPropertyAccessors(PropertyAccessor...)
268+
* @see #forReadOnlyDataBinding()
269+
* @see #forReadWriteDataBinding()
270+
*/
271+
@Override
272+
public boolean isAssignmentEnabled() {
273+
return this.assignmentEnabled;
274+
}
256275

257276
/**
258277
* Create a {@code SimpleEvaluationContext} for the specified {@link PropertyAccessor}
259278
* delegates: typically a custom {@code PropertyAccessor} specific to a use case
260279
* (e.g. attribute resolution in a custom data structure), potentially combined with
261280
* a {@link DataBindingPropertyAccessor} if property dereferences are needed as well.
281+
* <p>Assignment is enabled within expressions evaluated by the context created via
282+
* this factory method.
262283
* @param accessors the accessor delegates to use
263284
* @see DataBindingPropertyAccessor#forReadOnlyAccess()
264285
* @see DataBindingPropertyAccessor#forReadWriteAccess()
286+
* @see #isAssignmentEnabled()
265287
*/
266288
public static Builder forPropertyAccessors(PropertyAccessor... accessors) {
267289
for (PropertyAccessor accessor : accessors) {
@@ -270,27 +292,33 @@ public static Builder forPropertyAccessors(PropertyAccessor... accessors) {
270292
"ReflectivePropertyAccessor. Consider using DataBindingPropertyAccessor or a custom subclass.");
271293
}
272294
}
273-
return new Builder(accessors);
295+
return new Builder(true, accessors);
274296
}
275297

276298
/**
277299
* Create a {@code SimpleEvaluationContext} for read-only access to
278300
* public properties via {@link DataBindingPropertyAccessor}.
301+
* <p>Assignment is disabled within expressions evaluated by the context created via
302+
* this factory method.
279303
* @see DataBindingPropertyAccessor#forReadOnlyAccess()
280304
* @see #forPropertyAccessors
305+
* @see #isAssignmentEnabled()
281306
*/
282307
public static Builder forReadOnlyDataBinding() {
283-
return new Builder(DataBindingPropertyAccessor.forReadOnlyAccess());
308+
return new Builder(false, DataBindingPropertyAccessor.forReadOnlyAccess());
284309
}
285310

286311
/**
287312
* Create a {@code SimpleEvaluationContext} for read-write access to
288313
* public properties via {@link DataBindingPropertyAccessor}.
314+
* <p>Assignment is enabled within expressions evaluated by the context created via
315+
* this factory method.
289316
* @see DataBindingPropertyAccessor#forReadWriteAccess()
290317
* @see #forPropertyAccessors
318+
* @see #isAssignmentEnabled()
291319
*/
292320
public static Builder forReadWriteDataBinding() {
293-
return new Builder(DataBindingPropertyAccessor.forReadWriteAccess());
321+
return new Builder(true, DataBindingPropertyAccessor.forReadWriteAccess());
294322
}
295323

296324

@@ -309,7 +337,10 @@ public static final class Builder {
309337
@Nullable
310338
private TypedValue rootObject;
311339

312-
private Builder(PropertyAccessor... accessors) {
340+
private final boolean assignmentEnabled;
341+
342+
private Builder(boolean assignmentEnabled, PropertyAccessor... accessors) {
343+
this.assignmentEnabled = assignmentEnabled;
313344
this.accessors = Arrays.asList(accessors);
314345
}
315346

@@ -391,7 +422,8 @@ public Builder withTypedRootObject(Object rootObject, TypeDescriptor typeDescrip
391422
}
392423

393424
public SimpleEvaluationContext build() {
394-
return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter, this.rootObject);
425+
return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter, this.rootObject,
426+
this.assignmentEnabled);
395427
}
396428
}
397429

spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
* @author Andy Clement
3333
* @since 4.1
3434
*/
35-
class CompilableMapAccessor implements CompilablePropertyAccessor {
35+
public class CompilableMapAccessor implements CompilablePropertyAccessor {
3636

3737
@Override
3838
public Class<?>[] getSpecificTargetClasses() {

spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,11 @@ void propertyReadOnly() {
188188

189189
assertThatSpelEvaluationException()
190190
.isThrownBy(() -> parser.parseExpression("name='p3'").getValue(context, target))
191-
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE);
191+
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.NOT_ASSIGNABLE);
192192

193193
assertThatSpelEvaluationException()
194194
.isThrownBy(() -> parser.parseExpression("['name']='p4'").getValue(context, target))
195-
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE);
195+
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.NOT_ASSIGNABLE);
196196
}
197197

198198
@Test
@@ -207,7 +207,7 @@ void propertyReadOnlyWithRecordStyle() {
207207

208208
assertThatSpelEvaluationException()
209209
.isThrownBy(() -> parser.parseExpression("name='p3'").getValue(context, target2))
210-
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE);
210+
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.NOT_ASSIGNABLE);
211211
}
212212

213213
@Test

0 commit comments

Comments
 (0)