Files or Directories Accessible to External Parties in org.springframework:spring-core
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
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
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 AbstractMappingConturlPathHelper.setUrlDecode(false);}-private boolean useJaf = JAF_PRESENT;+private boolean useJaf = true;/**@@ -109,7 +109,7 @@ protected void handleMatch(String extension, MediaType mediaType) {@Overrideprotected 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 {@Beforepublic 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 AbstractMeprivate 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_APPLICATselectedMediaType = 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 produreturn (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
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 IOExceString 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.+*/@Overridepublic 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);}-@Overridepublic ContentNegotiationManager getObject() {return this.contentNegotiationManager;
spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java+4 −4
@@ -66,7 +66,8 @@ public class PathExtensionContentNegotiationStrategyPATH_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 {@Beforepublic 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-10513this.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+@Testpublic 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);+}}}
Protect against RFD exploits
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 IOExceString 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 AbstractMappingConturlPathHelper.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 {@Beforepublic 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-10513this.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+@Testpublic 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
References
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2015-5211
- WEBhttps://github.com/spring-projects/spring-framework/commit/03f547eb9868f48f44d59b56067d4ac4740672c3
- WEBhttps://github.com/spring-projects/spring-framework/commit/2bd1daa75ee0b8ec33608ca6ab065ef3e1815543
- WEBhttps://github.com/spring-projects/spring-framework/commit/a95c3d820dbc4c3ae752f1b3ee22ee860b162402
- ADVISORYhttps://github.com/advisories/GHSA-pgf9-h69p-pcgf
- PACKAGEhttps://github.com/spring-projects/spring-framework
- WEBhttps://lists.debian.org/debian-lts-announce/2019/07/msg00012.html
- WEBhttps://pivotal.io/security/cve-2015-5211
- WEBhttps://www.trustwave.com/Resources/SpiderLabs-Blog/Reflected-File-Download---A-New-Web-Attack-Vector