Skip to content

Commit cbbb287

Browse files
sbrannenbclozel
authored andcommitted
Limit string concatenation in SpEL expressions
This commit introduces support for limiting the maximum length of a string resulting from the concatenation operator (+) in SpEL expressions. Closes gh-30332
1 parent 18403cd commit cbbb287

File tree

3 files changed

+104
-17
lines changed

3 files changed

+104
-17
lines changed

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,11 @@ public enum SpelMessage {
268268

269269
/** @since 5.2.23 */
270270
MAX_REGEX_LENGTH_EXCEEDED(Kind.ERROR, 1077,
271-
"Regular expression contains too many characters, exceeding the threshold of ''{0}''");
271+
"Regular expression contains too many characters, exceeding the threshold of ''{0}''"),
272+
273+
/** @since 5.2.24 */
274+
MAX_CONCATENATED_STRING_LENGTH_EXCEEDED(Kind.ERROR, 1078,
275+
"Concatenated string is too long, exceeding the threshold of ''{0}'' characters");
272276

273277

274278
private final Kind kind;

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

+39-6
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-2023 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.
@@ -27,6 +27,8 @@
2727
import org.springframework.expression.TypedValue;
2828
import org.springframework.expression.spel.CodeFlow;
2929
import org.springframework.expression.spel.ExpressionState;
30+
import org.springframework.expression.spel.SpelEvaluationException;
31+
import org.springframework.expression.spel.SpelMessage;
3032
import org.springframework.lang.Nullable;
3133
import org.springframework.util.Assert;
3234
import org.springframework.util.NumberUtils;
@@ -46,10 +48,18 @@
4648
* @author Juergen Hoeller
4749
* @author Ivo Smid
4850
* @author Giovanni Dall'Oglio Risso
51+
* @author Sam Brannen
4952
* @since 3.0
5053
*/
5154
public class OpPlus extends Operator {
5255

56+
/**
57+
* Maximum number of characters permitted in a concatenated string.
58+
* @since 5.2.24
59+
*/
60+
private static final int MAX_CONCATENATED_STRING_LENGTH = 100_000;
61+
62+
5363
public OpPlus(int startPos, int endPos, SpelNodeImpl... operands) {
5464
super("+", startPos, endPos, operands);
5565
Assert.notEmpty(operands, "Operands must not be empty");
@@ -123,22 +133,45 @@ else if (CodeFlow.isIntegerForNumericOp(leftNumber) || CodeFlow.isIntegerForNume
123133

124134
if (leftOperand instanceof String && rightOperand instanceof String) {
125135
this.exitTypeDescriptor = "Ljava/lang/String";
126-
return new TypedValue((String) leftOperand + rightOperand);
136+
String leftString = (String) leftOperand;
137+
String rightString = (String) rightOperand;
138+
checkStringLength(leftString);
139+
checkStringLength(rightString);
140+
return concatenate(leftString, rightString);
127141
}
128142

129143
if (leftOperand instanceof String) {
130-
return new TypedValue(
131-
leftOperand + (rightOperand == null ? "null" : convertTypedValueToString(operandTwoValue, state)));
144+
String leftString = (String) leftOperand;
145+
checkStringLength(leftString);
146+
String rightString = (rightOperand == null ? "null" : convertTypedValueToString(operandTwoValue, state));
147+
checkStringLength(rightString);
148+
return concatenate(leftString, rightString);
132149
}
133150

134151
if (rightOperand instanceof String) {
135-
return new TypedValue(
136-
(leftOperand == null ? "null" : convertTypedValueToString(operandOneValue, state)) + rightOperand);
152+
String rightString = (String) rightOperand;
153+
checkStringLength(rightString);
154+
String leftString = (leftOperand == null ? "null" : convertTypedValueToString(operandOneValue, state));
155+
checkStringLength(leftString);
156+
return concatenate(leftString, rightString);
137157
}
138158

139159
return state.operate(Operation.ADD, leftOperand, rightOperand);
140160
}
141161

162+
private void checkStringLength(String string) {
163+
if (string.length() > MAX_CONCATENATED_STRING_LENGTH) {
164+
throw new SpelEvaluationException(getStartPosition(),
165+
SpelMessage.MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, MAX_CONCATENATED_STRING_LENGTH);
166+
}
167+
}
168+
169+
private TypedValue concatenate(String leftString, String rightString) {
170+
String result = leftString + rightString;
171+
checkStringLength(result);
172+
return new TypedValue(result);
173+
}
174+
142175
@Override
143176
public String toStringAST() {
144177
if (this.children.length < 2) { // unary plus

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

+60-10
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.expression.spel.standard.SpelExpression;
2727

2828
import static org.assertj.core.api.Assertions.assertThat;
29+
import static org.springframework.expression.spel.SpelMessage.MAX_CONCATENATED_STRING_LENGTH_EXCEEDED;
2930
import static org.springframework.expression.spel.SpelMessage.MAX_REPEATED_TEXT_SIZE_EXCEEDED;
3031

3132
/**
@@ -391,11 +392,7 @@ public void testPlus() throws Exception {
391392
evaluate("3.0f + 5.0f", 8.0f, Float.class);
392393
evaluate("3.0d + 5.0d", 8.0d, Double.class);
393394
evaluate("3 + new java.math.BigDecimal('5')", new BigDecimal("8"), BigDecimal.class);
394-
395-
evaluate("'ab' + 2", "ab2", String.class);
396-
evaluate("2 + 'a'", "2a", String.class);
397-
evaluate("'ab' + null", "abnull", String.class);
398-
evaluate("null + 'ab'", "nullab", String.class);
395+
evaluate("5 + new Integer('37')", 42, Integer.class);
399396

400397
// AST:
401398
SpelExpression expr = (SpelExpression)parser.parseExpression("+3");
@@ -404,11 +401,11 @@ public void testPlus() throws Exception {
404401
assertThat(expr.toStringAST()).isEqualTo("(2 + 3)");
405402

406403
// use as a unary operator
407-
evaluate("+5d",5d,Double.class);
408-
evaluate("+5L",5L,Long.class);
409-
evaluate("+5",5,Integer.class);
410-
evaluate("+new java.math.BigDecimal('5')", new BigDecimal("5"),BigDecimal.class);
411-
evaluateAndCheckError("+'abc'",SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
404+
evaluate("+5d", 5d, Double.class);
405+
evaluate("+5L", 5L, Long.class);
406+
evaluate("+5", 5, Integer.class);
407+
evaluate("+new java.math.BigDecimal('5')", new BigDecimal("5"), BigDecimal.class);
408+
evaluateAndCheckError("+'abc'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
412409

413410
// string concatenation
414411
evaluate("'abc'+'def'","abcdef",String.class);
@@ -587,6 +584,59 @@ void stringRepeat() {
587584
evaluateAndCheckError("'a' * 257", String.class, MAX_REPEATED_TEXT_SIZE_EXCEEDED, 4);
588585
}
589586

587+
@Test
588+
void stringConcatenation() {
589+
evaluate("'' + ''", "", String.class);
590+
evaluate("'' + null", "null", String.class);
591+
evaluate("null + ''", "null", String.class);
592+
evaluate("'ab' + null", "abnull", String.class);
593+
evaluate("null + 'ab'", "nullab", String.class);
594+
evaluate("'ab' + 2", "ab2", String.class);
595+
evaluate("2 + 'ab'", "2ab", String.class);
596+
evaluate("'abc' + 'def'", "abcdef", String.class);
597+
598+
// Text is big but not too big
599+
final int maxSize = 100_000;
600+
context.setVariable("text1", createString(maxSize));
601+
Expression expr = parser.parseExpression("#text1 + ''");
602+
assertThat(expr.getValue(context, String.class)).hasSize(maxSize);
603+
604+
expr = parser.parseExpression("'' + #text1");
605+
assertThat(expr.getValue(context, String.class)).hasSize(maxSize);
606+
607+
context.setVariable("text1", createString(maxSize / 2));
608+
expr = parser.parseExpression("#text1 + #text1");
609+
assertThat(expr.getValue(context, String.class)).hasSize(maxSize);
610+
611+
// Text is too big
612+
context.setVariable("text1", createString(maxSize + 1));
613+
evaluateAndCheckError("#text1 + ''", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
614+
evaluateAndCheckError("#text1 + true", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
615+
evaluateAndCheckError("'' + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 3);
616+
evaluateAndCheckError("true + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 5);
617+
618+
context.setVariable("text1", createString(maxSize / 2));
619+
context.setVariable("text2", createString((maxSize / 2) + 1));
620+
evaluateAndCheckError("#text1 + #text2", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
621+
evaluateAndCheckError("#text1 + #text2 + true", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
622+
evaluateAndCheckError("#text1 + true + #text2", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 14);
623+
evaluateAndCheckError("true + #text1 + #text2", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 14);
624+
625+
evaluateAndCheckError("#text2 + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
626+
evaluateAndCheckError("#text2 + #text1 + true", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
627+
evaluateAndCheckError("#text2 + true + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 14);
628+
evaluateAndCheckError("true + #text2 + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 14);
629+
630+
context.setVariable("text1", createString((maxSize / 3) + 1));
631+
evaluateAndCheckError("#text1 + #text1 + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 16);
632+
evaluateAndCheckError("(#text1 + #text1) + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 18);
633+
evaluateAndCheckError("#text1 + (#text1 + #text1)", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
634+
}
635+
636+
private static String createString(int size) {
637+
return new String(new char[size]);
638+
}
639+
590640
@Test
591641
public void testLongs() {
592642
evaluate("3L == 4L", false, Boolean.class);

0 commit comments

Comments
 (0)