Skip to content

Commit

Permalink
Protect against RFD exploits
Browse files Browse the repository at this point in the history
Issue: SPR-13548
  • Loading branch information
rstoyanchev authored and snicoll committed Oct 15, 2015
1 parent 161fd98 commit 2bd1daa
Show file tree
Hide file tree
Showing 20 changed files with 465 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 + "(");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -272,7 +276,6 @@ public void afterPropertiesSet() {
this.contentNegotiationManager = new ContentNegotiationManager(strategies);
}


@Override
public ContentNegotiationManager getObject() {
return this.contentNegotiationManager;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ public class PathExtensionContentNegotiationStrategy
PATH_HELPER.setUrlDecode(false);
}

private boolean useJaf = JAF_PRESENT;

private boolean useJaf = true;

private boolean ignoreUnknownExtensions = true;

Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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\"")));
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -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.
Expand All @@ -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();
Expand All @@ -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");

Expand All @@ -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();
Expand All @@ -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
Expand Down Expand Up @@ -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);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -16,8 +16,12 @@

package org.springframework.web.servlet.mvc.method.annotation;

import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJacksonValue;
Expand All @@ -44,6 +48,14 @@
*/
public abstract class AbstractJsonpResponseBodyAdvice extends AbstractMappingJacksonResponseBodyAdvice {

/**
* Pattern for validating jsonp callback parameter values.
*/
private static final Pattern CALLBACK_PARAM_PATTERN = Pattern.compile("[0-9A-Za-z_\\.]*");


private final Log logger = LogFactory.getLog(getClass());

private final String[] jsonpQueryParamNames;


Expand All @@ -62,14 +74,31 @@ protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaT
for (String name : this.jsonpQueryParamNames) {
String value = servletRequest.getParameter(name);
if (value != null) {
if (!isValidJsonpQueryParam(value)) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid jsonp parameter value: " + value);
}
continue;
}
MediaType contentTypeToUse = getContentType(contentType, request, response);
response.getHeaders().setContentType(contentTypeToUse);
bodyContainer.setJsonpFunction(value);
return;
break;
}
}
}

/**
* Validate the jsonp query parameter value. The default implementation
* returns true if it consists of digits, letters, or "_" and ".".
* Invalid parameter values are ignored.
* @param value the query param value, never {@code null}
* @since 4.1.8
*/
protected boolean isValidJsonpQueryParam(String value) {
return CALLBACK_PARAM_PATTERN.matcher(value).matches();
}

/**
* Return the content type to set the response to.
* This implementation always returns "application/javascript".
Expand Down
Loading

0 comments on commit 2bd1daa

Please # to comment.