Skip to content

Commit 8645746

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-30331
1 parent be129dc commit 8645746

File tree

3 files changed

+99
-17
lines changed

3 files changed

+99
-17
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: 39 additions & 6 deletions
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

Lines changed: 55 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
/**
@@ -391,11 +392,7 @@ void plus() {
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");
@@ -409,11 +406,6 @@ void plus() {
409406
evaluate("+5", 5, Integer.class);
410407
evaluate("+new java.math.BigDecimal('5')", new BigDecimal("5"), BigDecimal.class);
411408
evaluateAndCheckError("+'abc'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
412-
413-
// string concatenation
414-
evaluate("'abc'+'def'", "abcdef", String.class);
415-
416-
evaluate("5 + new Integer('37')", 42, Integer.class);
417409
}
418410

419411
@Test
@@ -587,6 +579,59 @@ void stringRepeat() {
587579
evaluateAndCheckError("'a' * 257", String.class, MAX_REPEATED_TEXT_SIZE_EXCEEDED, 4);
588580
}
589581

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

0 commit comments

Comments
 (0)