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 271e5fd commit a95c3d8
Show file tree
Hide file tree
Showing 19 changed files with 419 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 + "(");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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

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

Please sign in to comment.