Skip to content

Commit bc1511d

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-30324
1 parent db9b139 commit bc1511d

File tree

3 files changed

+98
-19
lines changed

3 files changed

+98
-19
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,11 @@ public enum SpelMessage {
272272

273273
/** @since 5.2.23 */
274274
MAX_REGEX_LENGTH_EXCEEDED(Kind.ERROR, 1077,
275-
"Regular expression contains too many characters, exceeding the threshold of ''{0}''");
275+
"Regular expression contains too many characters, exceeding the threshold of ''{0}''"),
276+
277+
/** @since 5.2.24 */
278+
MAX_CONCATENATED_STRING_LENGTH_EXCEEDED(Kind.ERROR, 1078,
279+
"Concatenated string is too long, exceeding the threshold of ''{0}'' characters");
276280

277281

278282
private final Kind kind;

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

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 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");
@@ -120,22 +130,41 @@ else if (CodeFlow.isIntegerForNumericOp(leftNumber) || CodeFlow.isIntegerForNume
120130

121131
if (leftOperand instanceof String leftString && rightOperand instanceof String rightString) {
122132
this.exitTypeDescriptor = "Ljava/lang/String";
123-
return new TypedValue(leftString + rightString);
133+
checkStringLength(leftString);
134+
checkStringLength(rightString);
135+
return concatenate(leftString, rightString);
124136
}
125137

126-
if (leftOperand instanceof String) {
127-
return new TypedValue(
128-
leftOperand + (rightOperand == null ? "null" : convertTypedValueToString(operandTwoValue, state)));
138+
if (leftOperand instanceof String leftString) {
139+
checkStringLength(leftString);
140+
String rightString = (rightOperand == null ? "null" : convertTypedValueToString(operandTwoValue, state));
141+
checkStringLength(rightString);
142+
return concatenate(leftString, rightString);
129143
}
130144

131-
if (rightOperand instanceof String) {
132-
return new TypedValue(
133-
(leftOperand == null ? "null" : convertTypedValueToString(operandOneValue, state)) + rightOperand);
145+
if (rightOperand instanceof String rightString) {
146+
checkStringLength(rightString);
147+
String leftString = (leftOperand == null ? "null" : convertTypedValueToString(operandOneValue, state));
148+
checkStringLength(leftString);
149+
return concatenate(leftString, rightString);
134150
}
135151

136152
return state.operate(Operation.ADD, leftOperand, rightOperand);
137153
}
138154

155+
private void checkStringLength(String string) {
156+
if (string.length() > MAX_CONCATENATED_STRING_LENGTH) {
157+
throw new SpelEvaluationException(getStartPosition(),
158+
SpelMessage.MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, MAX_CONCATENATED_STRING_LENGTH);
159+
}
160+
}
161+
162+
private TypedValue concatenate(String leftString, String rightString) {
163+
String result = leftString + rightString;
164+
checkStringLength(result);
165+
return new TypedValue(result);
166+
}
167+
139168
@Override
140169
public String toStringAST() {
141170
if (this.children.length < 2) { // unary plus

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

Lines changed: 56 additions & 10 deletions
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
/**
@@ -34,6 +35,7 @@
3435
* @author Andy Clement
3536
* @author Juergen Hoeller
3637
* @author Giovanni Dall'Oglio Risso
38+
* @author Sam Brannen
3739
*/
3840
class OperatorTests extends AbstractExpressionTests {
3941

@@ -392,11 +394,7 @@ void plus() {
392394
evaluate("3.0f + 5.0f", 8.0f, Float.class);
393395
evaluate("3.0d + 5.0d", 8.0d, Double.class);
394396
evaluate("3 + new java.math.BigDecimal('5')", new BigDecimal("8"), BigDecimal.class);
395-
396-
evaluate("'ab' + 2", "ab2", String.class);
397-
evaluate("2 + 'a'", "2a", String.class);
398-
evaluate("'ab' + null", "abnull", String.class);
399-
evaluate("null + 'ab'", "nullab", String.class);
397+
evaluate("5 + new Integer('37')", 42, Integer.class);
400398

401399
// AST:
402400
SpelExpression expr = (SpelExpression) parser.parseExpression("+3");
@@ -410,11 +408,6 @@ void plus() {
410408
evaluate("+5", 5, Integer.class);
411409
evaluate("+new java.math.BigDecimal('5')", new BigDecimal("5"), BigDecimal.class);
412410
evaluateAndCheckError("+'abc'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
413-
414-
// string concatenation
415-
evaluate("'abc'+'def'", "abcdef", String.class);
416-
417-
evaluate("5 + new Integer('37')", 42, Integer.class);
418411
}
419412

420413
@Test
@@ -588,6 +581,59 @@ void stringRepeat() {
588581
evaluateAndCheckError("'a' * 257", String.class, MAX_REPEATED_TEXT_SIZE_EXCEEDED, 4);
589582
}
590583

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

0 commit comments

Comments
 (0)