Security context
High· 8.6GHSA-pgf9-h69p-pcgf CVE-2015-5211CWE-20CWE-552Published Oct 17, 2018

Files or Directories Accessible to External Parties in org.springframework:spring-core

Research this vulnerability

Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.

Affected versions

4.2.0 → fixed in 4.2.24.0.0 → fixed in 4.1.80 → fixed in 3.2.15

Details

Under some situations, the Spring Framework 4.2.0 to 4.2.1, 4.0.0 to 4.1.7, 3.2.0 to 3.2.14 and older unsupported versions is vulnerable to a Reflected File Download (RFD) attack. The attack involves a malicious user crafting a URL with a batch script extension that results in the response being downloaded rather than rendered and also includes some input reflected in the response.

The fix

Protect against RFD exploits

Rossen Stoyanchev· Oct 9, 2015, 06:08 PM+1951503f547eb98
spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java+5 1
@@ -128,6 +128,10 @@ public void setUseJaf(boolean useJaf) {
this.useJaf = useJaf;
}
+ private boolean isUseJafTurnedOff() {
+ return (this.useJaf != null && !this.useJaf);
+ }
+
/**
* Indicate whether a request parameter should be used to determine the
* requested media type with the <em>2nd highest priority</em>, i.e.
@@ -184,7 +188,7 @@ public void afterPropertiesSet() {
if (this.favorPathExtension) {
PathExtensionContentNegotiationStrategy strategy;
- if (this.servletContext != null) {
+ if (this.servletContext != null && !isUseJafTurnedOff()) {
strategy = new ServletPathExtensionContentNegotiationStrategy(this.servletContext, this.mediaTypes);
}
else {
spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java+2 2
@@ -63,7 +63,7 @@ public class PathExtensionContentNegotiationStrategy extends AbstractMappingCont
urlPathHelper.setUrlDecode(false);
}
- private boolean useJaf = JAF_PRESENT;
+ private boolean useJaf = true;
/**
@@ -109,7 +109,7 @@ protected void handleMatch(String extension, MediaType mediaType) {
@Override
protected MediaType handleNoMatch(NativeWebRequest webRequest, String extension) {
- if (this.useJaf) {
+ if (this.useJaf && JAF_PRESENT) {
MediaType jafMediaType = JafMediaTypeFactory.getMediaType("file." + extension);
if (jafMediaType != null && !MediaType.APPLICATION_OCTET_STREAM.equals(jafMediaType)) {
return jafMediaType;
spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java+1 1
@@ -405,7 +405,7 @@ private String decodeAndCleanUriString(HttpServletRequest request, String uri) {
* @see java.net.URLDecoder#decode(String)
*/
public String decodeRequestString(HttpServletRequest request, String source) {
- if (this.urlDecode) {
+ if (this.urlDecode && source != null) {
return decodeInternal(request, source);
}
return source;
spring-web/src/main/java/org/springframework/web/util/WebUtils.java+7 4
@@ -709,20 +709,23 @@ public static String extractFilenameFromUrlPath(String urlPath) {
}
/**
- * Extract the full URL filename (including file extension) from the given request URL path.
- * Correctly resolves nested paths such as "/products/view.html" as well.
+ * Extract the full URL filename (including file extension) from the given
+ * request URL path. Correctly resolve nested paths such as
+ * "/products/view.html" and remove any path and or query parameters.
* @param urlPath the request URL path (e.g. "/products/index.html")
* @return the extracted URI filename (e.g. "index.html")
*/
public static String extractFullFilenameFromUrlPath(String urlPath) {
- int end = urlPath.indexOf(';');
+ int end = urlPath.indexOf('?');
if (end == -1) {
- end = urlPath.indexOf('?');
+ end = urlPath.indexOf('#');
if (end == -1) {
end = urlPath.length();
}
}
int begin = urlPath.lastIndexOf('/', end) + 1;
+ int paramIndex = urlPath.indexOf(';', begin);
+ end = (paramIndex != -1 && paramIndex < end ? paramIndex : end);
return urlPath.substring(begin, end);
}
spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java+49 7
@@ -26,6 +26,8 @@
import org.junit.Test;
import org.springframework.http.MediaType;
import org.springframework.mock.web.test.MockHttpServletRequest;
+import org.springframework.mock.web.test.MockServletContext;
+import org.springframework.util.StringUtils;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
@@ -43,7 +45,10 @@ public class ContentNegotiationManagerFactoryBeanTests {
@Before
public void setup() {
- this.servletRequest = new MockHttpServletRequest();
+ TestServletContext servletContext = new TestServletContext();
+ servletContext.getMimeTypes().put("foo", "application/foo");
+
+ this.servletRequest = new MockHttpServletRequest(servletContext);
this.webRequest = new ServletWebRequest(this.servletRequest);
this.factoryBean = new ContentNegotiationManagerFactoryBean();
@@ -74,16 +79,36 @@ public void defaultSettings() throws Exception {
}
@Test
- public void addMediaTypes() throws Exception {
- Map<String, MediaType> mediaTypes = new HashMap<String, MediaType>();
- mediaTypes.put("json", MediaType.APPLICATION_JSON);
- this.factoryBean.addMediaTypes(mediaTypes);
+ public void favorPath() throws Exception {
+ this.factoryBean.setFavorPathExtension(true);
+ this.factoryBean.addMediaTypes(Collections.singletonMap("bar", new MediaType("application", "bar")));
+ this.factoryBean.afterPropertiesSet();
+ ContentNegotiationManager manager = this.factoryBean.getObject();
+ this.servletRequest.setRequestURI("/flower.foo");
+ assertEquals(Collections.singletonList(new MediaType("application", "foo")),
+ manager.resolveMediaTypes(this.webRequest));
+
+ this.servletRequest.setRequestURI("/flower.bar");
+ assertEquals(Collections.singletonList(new MediaType("application", "bar")),
+ manager.resolveMediaTypes(this.webRequest));
+
+ this.servletRequest.setRequestURI("/flower.gif");
+ assertEquals(Collections.singletonList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest));
+ }
+
+ @Test
+ public void favorPathWithJafTurnedOff() throws Exception {
+ this.factoryBean.setFavorPathExtension(true);
+ this.factoryBean.setUseJaf(false);
this.factoryBean.afterPropertiesSet();
ContentNegotiationManager manager = this.factoryBean.getObject();
- this.servletRequest.setRequestURI("/flower.json");
- assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
+ this.servletRequest.setRequestURI("/flower.foo");
+ assertEquals(Collections.emptyList(), manager.resolveMediaTypes(this.webRequest));
+
+ this.servletRequest.setRequestURI("/flower.gif");
+ assertEquals(Collections.emptyList(), manager.resolveMediaTypes(this.webRequest));
}
@Test
@@ -130,4 +155,21 @@ public void setDefaultContentType() throws Exception {
assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
}
+
+ private static class TestServletContext extends MockServletContext {
+
+ private final Map<String, String> mimeTypes = new HashMap<>();
+
+
+ public Map<String, String> getMimeTypes() {
+ return this.mimeTypes;
+ }
+
+ @Override
+ public String getMimeType(String filePath) {
+ String extension = StringUtils.getFilenameExtension(filePath);
+ return getMimeTypes().get(extension);
+ }
+ }
+
}
spring-web/src/test/java/org/springframework/web/util/WebUtilsTests.java+8 0
@@ -62,9 +62,17 @@ public void extractFullFilenameFromUrlPath() {
assertEquals("index.html", WebUtils.extractFullFilenameFromUrlPath("index.html"));
assertEquals("index.html", WebUtils.extractFullFilenameFromUrlPath("/index.html"));
assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html"));
+ assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html#/a"));
+ assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html#/path/a"));
+ assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html#/path/a.do"));
assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html?param=a"));
assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html?param=/path/a"));
assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html?param=/path/a.do"));
+ assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html?param=/path/a#/path/a"));
+ assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html?param=/path/a.do#/path/a.do"));
+ assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products;q=11/view.html?param=/path/a.do"));
+ assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products;q=11/view.html;r=22?param=/path/a.do"));
+ assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products;q=11/view.html;r=22;s=33?param=/path/a.do"));
}
@Test
spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java+69 0
@@ -18,26 +18,32 @@
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.MethodParameter;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerMapping;
+import org.springframework.web.util.UrlPathHelper;
/**
* Extends {@link AbstractMessageConverterMethodArgumentResolver} with the ability to handle
@@ -52,8 +58,24 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application");
+ private static final UrlPathHelper RAW_URL_PATH_HELPER = new UrlPathHelper();
+
+ private static final UrlPathHelper DECODING_URL_PATH_HELPER = new UrlPathHelper();
+
+ static {
+ RAW_URL_PATH_HELPER.setRemoveSemicolonContent(false);
+ RAW_URL_PATH_HELPER.setUrlDecode(false);
+ }
+
+ /* Extensions associated with the built-in message converters */
+ private static final Set<String> WHITELISTED_EXTENSIONS = new HashSet<String>(Arrays.asList(
+ "txt", "text", "json", "xml", "atom", "rss", "png", "jpe", "jpeg", "jpg", "gif", "wbmp", "bmp"));
+
+
private final ContentNegotiationManager contentNegotiationManager;
+ private final Set<String> safeExtensions = new HashSet<String>();
+
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters) {
this(messageConverters, null);
@@ -64,6 +86,8 @@ protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>>
super(messageConverters);
this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager());
+ this.safeExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions());
+ this.safeExtensions.addAll(WHITELISTED_EXTENSIONS);
}
@@ -140,6 +164,7 @@ else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICAT
selectedMediaType = selectedMediaType.removeQualityValue();
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter.canWrite(returnValueClass, selectedMediaType)) {
+ addContentDispositionHeader(inputMessage, outputMessage);
((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" +
@@ -194,4 +219,48 @@ private MediaType getMostSpecificMediaType(MediaType acceptType, MediaType produ
return (MediaType.SPECIFICITY_COMPARATOR.compare(acceptType, produceTypeToUse) <= 0 ? acceptType : produceTypeToUse);
}
+ /**
+ * Check if the path has a file extension and whether the extension is either
+ * {@link #WHITELISTED_EXTENSIONS whitelisted} or
+ * {@link ContentNegotiationManager#getAllFileExtensions() explicitly
+ * registered}. If not add a 'Content-Disposition' header with a safe
+ * attachment file name ("f.txt") to prevent RFD exploits.
+ */
+ private void addContentDispositionHeader(ServletServerHttpRequest request,
+ ServletServerHttpResponse response) {
+
+ HttpHeaders headers = response.getHeaders();
+ if (headers.containsKey("Content-Disposition")) {
+ return;
+ }
+
+ HttpServletRequest servletRequest = request.getServletRequest();
+ String requestUri = RAW_URL_PATH_HELPER.getOriginatingRequestUri(servletRequest);
+
+ int index = requestUri.lastIndexOf('/') + 1;
+ String filename = requestUri.substring(index);
+ String pathParams = "";
+
+ index = filename.indexOf(';');
+ if (index != -1) {
+ pathParams = filename.substring(index);
+ filename = filename.substring(0, index);
+ }
+
+ filename = DECODING_URL_PATH_HELPER.decodeRequestString(servletRequest, filename);
+ String ext = StringUtils.getFilenameExtension(filename);
+
+ pathParams = DECODING_URL_PATH_HELPER.decodeRequestString(servletRequest, pathParams);
+ String extInPathParams = StringUtils.getFilenameExtension(pathParams);
+
+ if (!isSafeExtension(ext) || !isSafeExtension(extInPathParams)) {
+ headers.add("Content-Disposition", "attachment;filename=f.txt");
+ }
+ }
+
+ private boolean isSafeExtension(String extension) {
+ return (!StringUtils.hasText(extension) ||
+ this.safeExtensions.contains(extension.toLowerCase(Locale.ENGLISH)));
+ }
+
}
spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java+54 0
@@ -19,6 +19,7 @@
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import org.junit.Before;
@@ -35,6 +36,7 @@
import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
+import org.springframework.web.accept.ContentNegotiationManagerFactoryBean;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.support.WebDataBinderFactory;
@@ -42,6 +44,7 @@
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.ModelAndViewContainer;
+import org.springframework.web.util.WebUtils;
import static org.junit.Assert.*;
@@ -224,6 +227,57 @@ public void handleReturnValueStringAcceptCharset() throws Exception {
assertEquals("text/plain;charset=UTF-8", servletResponse.getHeader("Content-Type"));
}
+ @Test
+ public void addContentDispositionHeader() throws Exception {
+
+ ContentNegotiationManagerFactoryBean factory = new ContentNegotiationManagerFactoryBean();
+ factory.addMediaType("pdf", new MediaType("application", "pdf"));
+ factory.afterPropertiesSet();
+
+ RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
+ Collections.<HttpMessageConverter<?>>singletonList(new StringHttpMessageConverter()),
+ factory.getObject());
+
+ assertContentDisposition(processor, false, "/hello.json", "whitelisted extension");
+ assertContentDisposition(processor, false, "/hello.pdf", "registered extension");
+ assertContentDisposition(processor, true, "/hello.dataless", "uknown extension");
+
+ // path parameters
+ assertContentDisposition(processor, false, "/hello.json;a=b", "path param shouldn't cause issue");
+ assertContentDisposition(processor, true, "/hello.json;a=b;setup.dataless", "uknown ext in path params");
+ assertContentDisposition(processor, true, "/hello.dataless;a=b;setup.json", "uknown ext in filename");
+ assertContentDisposition(processor, false, "/hello.json;a=b;setup.json", "whitelisted extensions");
+
+ // encoded dot
+ assertContentDisposition(processor, true, "/hello%2Edataless;a=b;setup.json", "encoded dot in filename");
+ assertContentDisposition(processor, true, "/hello.json;a=b;setup%2Edataless", "encoded dot in path params");
+ assertContentDisposition(processor, true, "/hello.dataless%3Bsetup.bat", "encoded dot in path params");
+
+ this.servletRequest.setAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE, "/hello.bat");
+ assertContentDisposition(processor, true, "/bonjour", "forwarded URL");
+ this.servletRequest.removeAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE);
+ }
+
+ private void assertContentDisposition(RequestResponseBodyMethodProcessor processor,
+ boolean expectContentDisposition, String requestURI, String comment) throws Exception {
+
+ this.servletRequest.setRequestURI(requestURI);
+ processor.handleReturnValue("body", this.returnTypeString, this.mavContainer, this.webRequest);
+
+ String header = servletResponse.getHeader("Content-Disposition");
+ if (expectContentDisposition) {
+ assertEquals("Expected 'Content-Disposition' header. Use case: '" + comment + "'",
+ "attachment;filename=f.txt", header);
+ }
+ else {
+ assertNull("Did not expect 'Content-Disposition' header. Use case: '" + comment + "'", header);
+ }
+
+ this.servletRequest = new MockHttpServletRequest();
+ this.servletResponse = new MockHttpServletResponse();
+ this.webRequest = new ServletWebRequest(servletRequest, servletResponse);
+ }
+
public String handle(
@RequestBody List<SimpleBean> list,

Protect against RFD exploits

Rossen Stoyanchev· Oct 5, 2015, 01:25 AM+95312bd1daa75e
spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java+1 0
@@ -95,6 +95,7 @@ protected void writePrefix(JsonGenerator generator, Object object) throws IOExce
String jsonpFunction =
(object instanceof MappingJacksonValue ? ((MappingJacksonValue) object).getJsonpFunction() : null);
if (jsonpFunction != null) {
+ generator.writeRaw("/**/");
generator.writeRaw(jsonpFunction + "(");
}
}
spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java+10 0
@@ -120,6 +120,16 @@ public List<String> resolveFileExtensions(MediaType mediaType) {
return new ArrayList<String>(result);
}
+ /**
+ * {@inheritDoc}
+ * <p>At startup this method returns extensions explicitly registered with
+ * either {@link PathExtensionContentNegotiationStrategy} or
+ * {@link ParameterContentNegotiationStrategy}. At runtime if there is a
+ * "path extension" strategy and its
+ * {@link PathExtensionContentNegotiationStrategy#setUseJaf(boolean)
+ * useJaf} property is set to "true", the list of extensions may
+ * increase as file extensions are resolved via JAF and cached.
+ */
@Override
public List<String> getAllFileExtensions() {
Set<String> result = new LinkedHashSet<String>();
spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java+5 2
@@ -178,6 +178,10 @@ public void setUseJaf(boolean useJaf) {
this.useJaf = useJaf;
}
+ private boolean isUseJafTurnedOff() {
+ return (this.useJaf != null && !this.useJaf);
+ }
+
/**
* Whether a request parameter ("format" by default) should be used to
* determine the requested media type. For this option to work you must
@@ -240,7 +244,7 @@ public void afterPropertiesSet() {
if (this.favorPathExtension) {
PathExtensionContentNegotiationStrategy strategy;
- if (this.servletContext != null) {
+ if (this.servletContext != null && !isUseJafTurnedOff()) {
strategy = new ServletPathExtensionContentNegotiationStrategy(
this.servletContext, this.mediaTypes);
}
@@ -272,7 +276,6 @@ public void afterPropertiesSet() {
this.contentNegotiationManager = new ContentNegotiationManager(strategies);
}
-
@Override
public ContentNegotiationManager getObject() {
return this.contentNegotiationManager;
spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java+4 4
@@ -66,7 +66,8 @@ public class PathExtensionContentNegotiationStrategy
PATH_HELPER.setUrlDecode(false);
}
- private boolean useJaf = JAF_PRESENT;
+
+ private boolean useJaf = true;
private boolean ignoreUnknownExtensions = true;
@@ -89,8 +90,7 @@ public PathExtensionContentNegotiationStrategy() {
/**
* Whether to use the Java Activation Framework to look up file extensions.
- * <p>By default if this property is not set JAF is present on the
- * classpath it will be used.
+ * <p>By default this is set to "true" but depends on JAF being present.
*/
public void setUseJaf(boolean useJaf) {
this.useJaf = useJaf;
@@ -123,7 +123,7 @@ protected String getMediaTypeKey(NativeWebRequest webRequest) {
protected MediaType handleNoMatch(NativeWebRequest webRequest, String extension)
throws HttpMediaTypeNotAcceptableException {
- if (this.useJaf) {
+ if (this.useJaf && JAF_PRESENT) {
MediaType mediaType = JafMediaTypeFactory.getMediaType("file." + extension);
if (mediaType != null && !MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) {
return mediaType;
spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java+1 1
@@ -438,7 +438,7 @@ private String decodeAndCleanUriString(HttpServletRequest request, String uri) {
* @see java.net.URLDecoder#decode(String)
*/
public String decodeRequestString(HttpServletRequest request, String source) {
- if (this.urlDecode) {
+ if (this.urlDecode && source != null) {
return decodeInternal(request, source);
}
return source;
spring-web/src/main/java/org/springframework/web/util/WebUtils.java+7 4
@@ -723,20 +723,23 @@ public static String extractFilenameFromUrlPath(String urlPath) {
}
/**
- * Extract the full URL filename (including file extension) from the given request URL path.
- * Correctly resolves nested paths such as "/products/view.html" as well.
+ * Extract the full URL filename (including file extension) from the given
+ * request URL path. Correctly resolve nested paths such as
+ * "/products/view.html" and remove any path and or query parameters.
* @param urlPath the request URL path (e.g. "/products/index.html")
* @return the extracted URI filename (e.g. "index.html")
*/
public static String extractFullFilenameFromUrlPath(String urlPath) {
- int end = urlPath.indexOf(';');
+ int end = urlPath.indexOf('?');
if (end == -1) {
- end = urlPath.indexOf('?');
+ end = urlPath.indexOf('#');
if (end == -1) {
end = urlPath.length();
}
}
int begin = urlPath.lastIndexOf('/', end) + 1;
+ int paramIndex = urlPath.indexOf(';', begin);
+ end = (paramIndex != -1 && paramIndex < end ? paramIndex : end);
return urlPath.substring(begin, end);
}
spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java+2 2
@@ -291,7 +291,7 @@ public void jsonp() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
this.converter.writeInternal(jacksonValue, null, outputMessage);
- assertEquals("callback(\"foo\");", outputMessage.getBodyAsString(Charset.forName("UTF-8")));
+ assertEquals("/**/callback(\"foo\");", outputMessage.getBodyAsString(Charset.forName("UTF-8")));
}
@Test
@@ -308,7 +308,7 @@ public void jsonpAndJsonView() throws Exception {
this.converter.writeInternal(jacksonValue, null, outputMessage);
String result = outputMessage.getBodyAsString(Charset.forName("UTF-8"));
- assertThat(result, startsWith("callback("));
+ assertThat(result, startsWith("/**/callback("));
assertThat(result, endsWith(");"));
assertThat(result, containsString("\"withView1\":\"with\""));
assertThat(result, not(containsString("\"withView2\":\"with\"")));
spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java+65 18
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,7 +15,6 @@
*/
package org.springframework.web.accept;
-import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -25,11 +24,13 @@
import org.springframework.http.MediaType;
import org.springframework.mock.web.test.MockHttpServletRequest;
+import org.springframework.mock.web.test.MockServletContext;
+import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
/**
* Test fixture for {@link ContentNegotiationManagerFactoryBean} tests.
@@ -46,7 +47,10 @@ public class ContentNegotiationManagerFactoryBeanTests {
@Before
public void setup() {
- this.servletRequest = new MockHttpServletRequest();
+ TestServletContext servletContext = new TestServletContext();
+ servletContext.getMimeTypes().put("foo", "application/foo");
+
+ this.servletRequest = new MockHttpServletRequest(servletContext);
this.webRequest = new ServletWebRequest(this.servletRequest);
this.factoryBean = new ContentNegotiationManagerFactoryBean();
@@ -62,7 +66,7 @@ public void defaultSettings() throws Exception {
this.servletRequest.setRequestURI("/flower.gif");
assertEquals("Should be able to resolve file extensions by default",
- Arrays.asList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest));
+ Collections.singletonList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest));
this.servletRequest.setRequestURI("/flower.xyz");
@@ -79,26 +83,46 @@ public void defaultSettings() throws Exception {
this.servletRequest.addHeader("Accept", MediaType.IMAGE_GIF_VALUE);
assertEquals("Should resolve Accept header by default",
- Arrays.asList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest));
+ Collections.singletonList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest));
}
@Test
- public void addMediaTypes() throws Exception {
- Map<String, MediaType> mediaTypes = new HashMap<>();
- mediaTypes.put("json", MediaType.APPLICATION_JSON);
- this.factoryBean.addMediaTypes(mediaTypes);
+ public void favorPath() throws Exception {
+ this.factoryBean.setFavorPathExtension(true);
+ this.factoryBean.addMediaTypes(Collections.singletonMap("bar", new MediaType("application", "bar")));
+ this.factoryBean.afterPropertiesSet();
+ ContentNegotiationManager manager = this.factoryBean.getObject();
+
+ this.servletRequest.setRequestURI("/flower.foo");
+ assertEquals(Collections.singletonList(new MediaType("application", "foo")),
+ manager.resolveMediaTypes(this.webRequest));
+
+ this.servletRequest.setRequestURI("/flower.bar");
+ assertEquals(Collections.singletonList(new MediaType("application", "bar")),
+ manager.resolveMediaTypes(this.webRequest));
+
+ this.servletRequest.setRequestURI("/flower.gif");
+ assertEquals(Collections.singletonList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest));
+ }
+ @Test
+ public void favorPathWithJafTurnedOff() throws Exception {
+ this.factoryBean.setFavorPathExtension(true);
+ this.factoryBean.setUseJaf(false);
this.factoryBean.afterPropertiesSet();
ContentNegotiationManager manager = this.factoryBean.getObject();
- this.servletRequest.setRequestURI("/flower.json");
- assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
+ this.servletRequest.setRequestURI("/flower.foo");
+ assertEquals(Collections.emptyList(), manager.resolveMediaTypes(this.webRequest));
+
+ this.servletRequest.setRequestURI("/flower.gif");
+ assertEquals(Collections.emptyList(), manager.resolveMediaTypes(this.webRequest));
}
// SPR-10170
@Test(expected = HttpMediaTypeNotAcceptableException.class)
- public void favorPathExtensionWithUnknownMediaType() throws Exception {
+ public void favorPathWithIgnoreUnknownPathExtensionTurnedOff() throws Exception {
this.factoryBean.setFavorPathExtension(true);
this.factoryBean.setIgnoreUnknownPathExtensions(false);
this.factoryBean.afterPropertiesSet();
@@ -124,7 +148,8 @@ public void favorParameter() throws Exception {
this.servletRequest.setRequestURI("/flower");
this.servletRequest.addParameter("format", "json");
- assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
+ assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON),
+ manager.resolveMediaTypes(this.webRequest));
}
// SPR-10170
@@ -159,26 +184,48 @@ public void setDefaultContentType() throws Exception {
this.factoryBean.afterPropertiesSet();
ContentNegotiationManager manager = this.factoryBean.getObject();
- assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
+ assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON),
+ manager.resolveMediaTypes(this.webRequest));
// SPR-10513
this.servletRequest.addHeader("Accept", MediaType.ALL_VALUE);
- assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
+ assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON),
+ manager.resolveMediaTypes(this.webRequest));
}
// SPR-12286
+
@Test
public void setDefaultContentTypeWithStrategy() throws Exception {
this.factoryBean.setDefaultContentTypeStrategy(new FixedContentNegotiationStrategy(MediaType.APPLICATION_JSON));
this.factoryBean.afterPropertiesSet();
ContentNegotiationManager manager = this.factoryBean.getObject();
- assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
+ assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON),
+ manager.resolveMediaTypes(this.webRequest));
this.servletRequest.addHeader("Accept", MediaType.ALL_VALUE);
- assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
+ assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON),
+ manager.resolveMediaTypes(this.webRequest));
+ }
+
+
+ private static class TestServletContext extends MockServletContext {
+
+ private final Map<String, String> mimeTypes = new HashMap<>();
+
+
+ public Map<String, String> getMimeTypes() {
+ return this.mimeTypes;
+ }
+
+ @Override
+ public String getMimeType(String filePath) {
+ String extension = StringUtils.getFilenameExtension(filePath);
+ return getMimeTypes().get(extension);
+ }
}
}
More files changed — see the full commit.

Protect against RFD exploits

Rossen Stoyanchev· Oct 9, 2015, 05:51 PM+9128a95c3d820d
spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java+1 0
@@ -96,6 +96,7 @@ protected void writePrefix(JsonGenerator generator, Object object) throws IOExce
String jsonpFunction =
(object instanceof MappingJacksonValue ? ((MappingJacksonValue) object).getJsonpFunction() : null);
if (jsonpFunction != null) {
+ generator.writeRaw("/**/");
generator.writeRaw(jsonpFunction + "(");
}
}
spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java+5 1
@@ -141,6 +141,10 @@ public void setUseJaf(boolean useJaf) {
this.useJaf = useJaf;
}
+ private boolean isUseJafTurnedOff() {
+ return (this.useJaf != null && !this.useJaf);
+ }
+
/**
* Indicate whether a request parameter should be used to determine the
* requested media type with the <em>2nd highest priority</em>, i.e.
@@ -211,7 +215,7 @@ public void afterPropertiesSet() {
if (this.favorPathExtension) {
PathExtensionContentNegotiationStrategy strategy;
- if (this.servletContext != null) {
+ if (this.servletContext != null && !isUseJafTurnedOff()) {
strategy = new ServletPathExtensionContentNegotiationStrategy(this.servletContext, this.mediaTypes);
}
else {
spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java+2 2
@@ -64,7 +64,7 @@ public class PathExtensionContentNegotiationStrategy extends AbstractMappingCont
urlPathHelper.setUrlDecode(false);
}
- private boolean useJaf = JAF_PRESENT;
+ private boolean useJaf = true;
private boolean ignoreUnknownExtensions = true;
@@ -130,7 +130,7 @@ protected void handleMatch(String extension, MediaType mediaType) {
protected MediaType handleNoMatch(NativeWebRequest webRequest, String extension)
throws HttpMediaTypeNotAcceptableException {
- if (this.useJaf) {
+ if (this.useJaf && JAF_PRESENT) {
MediaType jafMediaType = JafMediaTypeFactory.getMediaType("file." + extension);
if (jafMediaType != null && !MediaType.APPLICATION_OCTET_STREAM.equals(jafMediaType)) {
return jafMediaType;
spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java+1 1
@@ -405,7 +405,7 @@ private String decodeAndCleanUriString(HttpServletRequest request, String uri) {
* @see java.net.URLDecoder#decode(String)
*/
public String decodeRequestString(HttpServletRequest request, String source) {
- if (this.urlDecode) {
+ if (this.urlDecode && source != null) {
return decodeInternal(request, source);
}
return source;
spring-web/src/main/java/org/springframework/web/util/WebUtils.java+7 4
@@ -728,20 +728,23 @@ public static String extractFilenameFromUrlPath(String urlPath) {
}
/**
- * Extract the full URL filename (including file extension) from the given request URL path.
- * Correctly resolves nested paths such as "/products/view.html" as well.
+ * Extract the full URL filename (including file extension) from the given
+ * request URL path. Correctly resolve nested paths such as
+ * "/products/view.html" and remove any path and or query parameters.
* @param urlPath the request URL path (e.g. "/products/index.html")
* @return the extracted URI filename (e.g. "index.html")
*/
public static String extractFullFilenameFromUrlPath(String urlPath) {
- int end = urlPath.indexOf(';');
+ int end = urlPath.indexOf('?');
if (end == -1) {
- end = urlPath.indexOf('?');
+ end = urlPath.indexOf('#');
if (end == -1) {
end = urlPath.length();
}
}
int begin = urlPath.lastIndexOf('/', end) + 1;
+ int paramIndex = urlPath.indexOf(';', begin);
+ end = (paramIndex != -1 && paramIndex < end ? paramIndex : end);
return urlPath.substring(begin, end);
}
spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java+2 2
@@ -267,7 +267,7 @@ public void jsonp() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
this.converter.writeInternal(jacksonValue, outputMessage);
- assertEquals("callback(\"foo\");", outputMessage.getBodyAsString(Charset.forName("UTF-8")));
+ assertEquals("/**/callback(\"foo\");", outputMessage.getBodyAsString(Charset.forName("UTF-8")));
}
@Test
@@ -284,7 +284,7 @@ public void jsonpAndJsonView() throws Exception {
this.converter.writeInternal(jacksonValue, outputMessage);
String result = outputMessage.getBodyAsString(Charset.forName("UTF-8"));
- assertThat(result, startsWith("callback("));
+ assertThat(result, startsWith("/**/callback("));
assertThat(result, endsWith(");"));
assertThat(result, containsString("\"withView1\":\"with\""));
assertThat(result, not(containsString("\"withView2\":\"with\"")));
spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java+65 18
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,7 +15,6 @@
*/
package org.springframework.web.accept;
-import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -25,11 +24,13 @@
import org.springframework.http.MediaType;
import org.springframework.mock.web.test.MockHttpServletRequest;
+import org.springframework.mock.web.test.MockServletContext;
+import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
/**
* Test fixture for {@link ContentNegotiationManagerFactoryBean} tests.
@@ -46,7 +47,10 @@ public class ContentNegotiationManagerFactoryBeanTests {
@Before
public void setup() {
- this.servletRequest = new MockHttpServletRequest();
+ TestServletContext servletContext = new TestServletContext();
+ servletContext.getMimeTypes().put("foo", "application/foo");
+
+ this.servletRequest = new MockHttpServletRequest(servletContext);
this.webRequest = new ServletWebRequest(this.servletRequest);
this.factoryBean = new ContentNegotiationManagerFactoryBean();
@@ -62,7 +66,7 @@ public void defaultSettings() throws Exception {
this.servletRequest.setRequestURI("/flower.gif");
assertEquals("Should be able to resolve file extensions by default",
- Arrays.asList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest));
+ Collections.singletonList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest));
this.servletRequest.setRequestURI("/flower.xyz");
@@ -79,26 +83,46 @@ public void defaultSettings() throws Exception {
this.servletRequest.addHeader("Accept", MediaType.IMAGE_GIF_VALUE);
assertEquals("Should resolve Accept header by default",
- Arrays.asList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest));
+ Collections.singletonList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest));
}
@Test
- public void addMediaTypes() throws Exception {
- Map<String, MediaType> mediaTypes = new HashMap<>();
- mediaTypes.put("json", MediaType.APPLICATION_JSON);
- this.factoryBean.addMediaTypes(mediaTypes);
+ public void favorPath() throws Exception {
+ this.factoryBean.setFavorPathExtension(true);
+ this.factoryBean.addMediaTypes(Collections.singletonMap("bar", new MediaType("application", "bar")));
+ this.factoryBean.afterPropertiesSet();
+ ContentNegotiationManager manager = this.factoryBean.getObject();
+
+ this.servletRequest.setRequestURI("/flower.foo");
+ assertEquals(Collections.singletonList(new MediaType("application", "foo")),
+ manager.resolveMediaTypes(this.webRequest));
+
+ this.servletRequest.setRequestURI("/flower.bar");
+ assertEquals(Collections.singletonList(new MediaType("application", "bar")),
+ manager.resolveMediaTypes(this.webRequest));
+
+ this.servletRequest.setRequestURI("/flower.gif");
+ assertEquals(Collections.singletonList(MediaType.IMAGE_GIF), manager.resolveMediaTypes(this.webRequest));
+ }
+ @Test
+ public void favorPathWithJafTurnedOff() throws Exception {
+ this.factoryBean.setFavorPathExtension(true);
+ this.factoryBean.setUseJaf(false);
this.factoryBean.afterPropertiesSet();
ContentNegotiationManager manager = this.factoryBean.getObject();
- this.servletRequest.setRequestURI("/flower.json");
- assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
+ this.servletRequest.setRequestURI("/flower.foo");
+ assertEquals(Collections.emptyList(), manager.resolveMediaTypes(this.webRequest));
+
+ this.servletRequest.setRequestURI("/flower.gif");
+ assertEquals(Collections.emptyList(), manager.resolveMediaTypes(this.webRequest));
}
// SPR-10170
@Test(expected = HttpMediaTypeNotAcceptableException.class)
- public void favorPathExtensionWithUnknownMediaType() throws Exception {
+ public void favorPathWithIgnoreUnknownPathExtensionTurnedOff() throws Exception {
this.factoryBean.setFavorPathExtension(true);
this.factoryBean.setIgnoreUnknownPathExtensions(false);
this.factoryBean.afterPropertiesSet();
@@ -124,7 +148,8 @@ public void favorParameter() throws Exception {
this.servletRequest.setRequestURI("/flower");
this.servletRequest.addParameter("format", "json");
- assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
+ assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON),
+ manager.resolveMediaTypes(this.webRequest));
}
// SPR-10170
@@ -159,26 +184,48 @@ public void setDefaultContentType() throws Exception {
this.factoryBean.afterPropertiesSet();
ContentNegotiationManager manager = this.factoryBean.getObject();
- assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
+ assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON),
+ manager.resolveMediaTypes(this.webRequest));
// SPR-10513
this.servletRequest.addHeader("Accept", MediaType.ALL_VALUE);
- assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
+ assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON),
+ manager.resolveMediaTypes(this.webRequest));
}
// SPR-12286
+
@Test
public void setDefaultContentTypeWithStrategy() throws Exception {
this.factoryBean.setDefaultContentTypeStrategy(new FixedContentNegotiationStrategy(MediaType.APPLICATION_JSON));
this.factoryBean.afterPropertiesSet();
ContentNegotiationManager manager = this.factoryBean.getObject();
- assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
+ assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON),
+ manager.resolveMediaTypes(this.webRequest));
this.servletRequest.addHeader("Accept", MediaType.ALL_VALUE);
- assertEquals(Arrays.asList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest));
+ assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON),
+ manager.resolveMediaTypes(this.webRequest));
+ }
+
+
+ private static class TestServletContext extends MockServletContext {
+
+ private final Map<String, String> mimeTypes = new HashMap<>();
+
+
+ public Map<String, String> getMimeTypes() {
+ return this.mimeTypes;
+ }
+
+ @Override
+ public String getMimeType(String filePath) {
+ String extension = StringUtils.getFilenameExtension(filePath);
+ return getMimeTypes().get(extension);
+ }
}
}
spring-web/src/test/java/org/springframework/web/util/WebUtilsTests.java+8 0
@@ -70,9 +70,17 @@ public void extractFullFilenameFromUrlPath() {
assertEquals("index.html", WebUtils.extractFullFilenameFromUrlPath("index.html"));
assertEquals("index.html", WebUtils.extractFullFilenameFromUrlPath("/index.html"));
assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html"));
+ assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html#/a"));
+ assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html#/path/a"));
+ assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html#/path/a.do"));
assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html?param=a"));
assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html?param=/path/a"));
assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html?param=/path/a.do"));
+ assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html?param=/path/a#/path/a"));
+ assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html?param=/path/a.do#/path/a.do"));
+ assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products;q=11/view.html?param=/path/a.do"));
+ assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products;q=11/view.html;r=22?param=/path/a.do"));
+ assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products;q=11/view.html;r=22;s=33?param=/path/a.do"));
}
@Test
More files changed — see the full commit.

References