Skip to content

Commit edb16a1

Browse files
committed
Protect against SpEL injections
Prevent potential SpEL injection attacks by ensuring that whitelabel error view SpEL placeholders are not recursively resolved. Fixes gh-4763
1 parent 7d5cc3d commit edb16a1

File tree

4 files changed

+187
-26
lines changed

4 files changed

+187
-26
lines changed

spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration.java

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.autoconfigure.web;
1818

19+
import java.util.Collections;
1920
import java.util.HashMap;
2021
import java.util.List;
2122
import java.util.Map;
@@ -51,10 +52,10 @@
5152
import org.springframework.core.Ordered;
5253
import org.springframework.core.io.support.SpringFactoriesLoader;
5354
import org.springframework.core.type.AnnotatedTypeMetadata;
55+
import org.springframework.expression.EvaluationContext;
5456
import org.springframework.expression.Expression;
5557
import org.springframework.expression.spel.standard.SpelExpressionParser;
5658
import org.springframework.expression.spel.support.StandardEvaluationContext;
57-
import org.springframework.util.PropertyPlaceholderHelper;
5859
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
5960
import org.springframework.web.servlet.DispatcherServlet;
6061
import org.springframework.web.servlet.View;
@@ -172,19 +173,18 @@ public ConditionOutcome getMatchOutcome(ConditionContext context,
172173
*/
173174
private static class SpelView implements View {
174175

175-
private final String template;
176-
177-
private final StandardEvaluationContext context = new StandardEvaluationContext();
176+
private final NonRecursivePropertyPlaceholderHelper helper;
178177

179-
private PropertyPlaceholderHelper helper;
178+
private final String template;
180179

181-
private PlaceholderResolver resolver;
180+
private final Map<String, Expression> expressions;
182181

183182
public SpelView(String template) {
183+
this.helper = new NonRecursivePropertyPlaceholderHelper("${", "}");
184184
this.template = template;
185-
this.context.addPropertyAccessor(new MapAccessor());
186-
this.helper = new PropertyPlaceholderHelper("${", "}");
187-
this.resolver = new SpelPlaceholderResolver(this.context);
185+
ExpressionCollector expressionCollector = new ExpressionCollector();
186+
this.helper.replacePlaceholders(this.template, expressionCollector);
187+
this.expressions = expressionCollector.getExpressions();
188188
}
189189

190190
@Override
@@ -200,36 +200,63 @@ public void render(Map<String, ?> model, HttpServletRequest request,
200200
}
201201
Map<String, Object> map = new HashMap<String, Object>(model);
202202
map.put("path", request.getContextPath());
203-
this.context.setRootObject(map);
204-
String result = this.helper.replacePlaceholders(this.template, this.resolver);
203+
PlaceholderResolver resolver = new ExpressionResolver(this.expressions, map);
204+
String result = this.helper.replacePlaceholders(this.template, resolver);
205205
response.getWriter().append(result);
206206
}
207207

208208
}
209209

210210
/**
211-
* SpEL based {@link PlaceholderResolver}.
211+
* {@link PlaceholderResolver} to collect placeholder expressions.
212212
*/
213-
private static class SpelPlaceholderResolver implements PlaceholderResolver {
213+
private static class ExpressionCollector implements PlaceholderResolver {
214214

215215
private final SpelExpressionParser parser = new SpelExpressionParser();
216216

217-
private final StandardEvaluationContext context;
217+
private final Map<String, Expression> expressions = new HashMap<String, Expression>();
218218

219-
public SpelPlaceholderResolver(StandardEvaluationContext context) {
220-
this.context = context;
219+
@Override
220+
public String resolvePlaceholder(String name) {
221+
this.expressions.put(name, this.parser.parseExpression(name));
222+
return null;
223+
}
224+
225+
public Map<String, Expression> getExpressions() {
226+
return Collections.unmodifiableMap(this.expressions);
227+
}
228+
229+
}
230+
231+
/**
232+
* SpEL based {@link PlaceholderResolver}.
233+
*/
234+
private static class ExpressionResolver implements PlaceholderResolver {
235+
236+
private final Map<String, Expression> expressions;
237+
238+
private final EvaluationContext context;
239+
240+
ExpressionResolver(Map<String, Expression> expressions, Map<String, ?> map) {
241+
this.expressions = expressions;
242+
this.context = getContext(map);
243+
}
244+
245+
private EvaluationContext getContext(Map<String, ?> map) {
246+
StandardEvaluationContext context = new StandardEvaluationContext();
247+
context.addPropertyAccessor(new MapAccessor());
248+
context.setRootObject(map);
249+
return context;
221250
}
222251

223252
@Override
224-
public String resolvePlaceholder(String name) {
225-
Expression expression = this.parser.parseExpression(name);
226-
try {
227-
Object value = expression.getValue(this.context);
228-
return HtmlUtils.htmlEscape(value == null ? null : value.toString());
229-
}
230-
catch (Exception ex) {
231-
return null;
232-
}
253+
public String resolvePlaceholder(String placeholderName) {
254+
Expression expression = this.expressions.get(placeholderName);
255+
return escape(expression == null ? null : expression.getValue(this.context));
256+
}
257+
258+
private String escape(Object value) {
259+
return HtmlUtils.htmlEscape(value == null ? null : value.toString());
233260
}
234261

235262
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2012-2015 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+
* http://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.boot.autoconfigure.web;
18+
19+
import java.util.Set;
20+
21+
import org.springframework.util.PropertyPlaceholderHelper;
22+
23+
/**
24+
* {@link PropertyPlaceholderHelper} that doesn't allow recursive resolution.
25+
*
26+
* @author Phillip Webb
27+
*/
28+
class NonRecursivePropertyPlaceholderHelper extends PropertyPlaceholderHelper {
29+
30+
NonRecursivePropertyPlaceholderHelper(String placeholderPrefix,
31+
String placeholderSuffix) {
32+
super(placeholderPrefix, placeholderSuffix);
33+
}
34+
35+
@Override
36+
protected String parseStringValue(String strVal,
37+
PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
38+
return super.parseStringValue(strVal,
39+
new NonRecursivePlaceholderResolver(placeholderResolver),
40+
visitedPlaceholders);
41+
}
42+
43+
private static class NonRecursivePlaceholderResolver implements PlaceholderResolver {
44+
45+
private final PlaceholderResolver resolver;
46+
47+
public NonRecursivePlaceholderResolver(PlaceholderResolver resolver) {
48+
this.resolver = resolver;
49+
}
50+
51+
@Override
52+
public String resolvePlaceholder(String placeholderName) {
53+
if (this.resolver instanceof NonRecursivePlaceholderResolver) {
54+
return null;
55+
}
56+
return this.resolver.resolvePlaceholder(placeholderName);
57+
}
58+
59+
}
60+
61+
}

spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/DefaultErrorViewIntegrationTests.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
4343
import org.springframework.web.context.WebApplicationContext;
4444

45+
import static org.junit.Assert.assertFalse;
4546
import static org.junit.Assert.assertTrue;
4647
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
4748
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -76,7 +77,7 @@ public void testErrorForBrowserClient() throws Exception {
7677
}
7778

7879
@Test
79-
public void testErrorWithEscape() throws Exception {
80+
public void testErrorWithHtmlEscape() throws Exception {
8081
MvcResult response = this.mockMvc
8182
.perform(get("/error")
8283
.requestAttr("javax.servlet.error.exception",
@@ -90,6 +91,21 @@ public void testErrorWithEscape() throws Exception {
9091
assertTrue("Wrong content: " + content, content.contains("999"));
9192
}
9293

94+
@Test
95+
public void testErrorWithSpelEscape() throws Exception {
96+
String spel = "${T(" + getClass().getName() + ").injectCall()}";
97+
MvcResult response = this.mockMvc
98+
.perform(
99+
get("/error")
100+
.requestAttr("javax.servlet.error.exception",
101+
new RuntimeException(spel))
102+
.accept(MediaType.TEXT_HTML))
103+
.andExpect(status().is5xxServerError()).andReturn();
104+
String content = response.getResponse().getContentAsString();
105+
System.out.println(content);
106+
assertFalse("Wrong content: " + content, content.contains("injection"));
107+
}
108+
93109
@Target(ElementType.TYPE)
94110
@Retention(RetentionPolicy.RUNTIME)
95111
@Documented
@@ -112,4 +128,8 @@ public static void main(String[] args) {
112128

113129
}
114130

131+
public static String injectCall() {
132+
return "injection";
133+
}
134+
115135
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2012-2015 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+
* http://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.boot.autoconfigure.web;
18+
19+
import java.util.Properties;
20+
21+
import org.junit.Test;
22+
23+
import static org.hamcrest.Matchers.equalTo;
24+
import static org.junit.Assert.assertThat;
25+
26+
/**
27+
* Tests for {@link NonRecursivePropertyPlaceholderHelper}.
28+
*
29+
* @author Phillip Webb
30+
*/
31+
public class NonRecursivePropertyPlaceholderHelperTests {
32+
33+
private final NonRecursivePropertyPlaceholderHelper helper = new NonRecursivePropertyPlaceholderHelper(
34+
"${", "}");
35+
36+
@Test
37+
public void canResolve() {
38+
Properties properties = new Properties();
39+
properties.put("a", "b");
40+
String result = this.helper.replacePlaceholders("${a}", properties);
41+
assertThat(result, equalTo("b"));
42+
}
43+
44+
@Test
45+
public void cannotResolveRecursive() {
46+
Properties properties = new Properties();
47+
properties.put("a", "${b}");
48+
properties.put("b", "c");
49+
String result = this.helper.replacePlaceholders("${a}", properties);
50+
assertThat(result, equalTo("${b}"));
51+
}
52+
53+
}

0 commit comments

Comments
 (0)