Protect against RFD exploits
Issue: SPR-13548
This commit is contained in:
committed by
Stephane Nicoll
parent
161fd98656
commit
2bd1daa75e
@@ -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.
|
||||
@@ -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;
|
||||
@@ -44,6 +48,14 @@ import org.springframework.util.ObjectUtils;
|
||||
*/
|
||||
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;
|
||||
|
||||
|
||||
@@ -62,14 +74,31 @@ public abstract class AbstractJsonpResponseBodyAdvice extends AbstractMappingJac
|
||||
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".
|
||||
|
||||
@@ -19,15 +19,19 @@ package org.springframework.web.servlet.mvc.method.annotation;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
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.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
@@ -36,12 +40,14 @@ import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
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
|
||||
@@ -56,24 +62,52 @@ 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>();
|
||||
|
||||
|
||||
/**
|
||||
* Constructor with list of converters only.
|
||||
*/
|
||||
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters) {
|
||||
this(converters, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with list of converters and ContentNegotiationManager.
|
||||
*/
|
||||
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters,
|
||||
ContentNegotiationManager contentNegotiationManager) {
|
||||
|
||||
this(converters, contentNegotiationManager, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with list of converters and ContentNegotiationManager as well
|
||||
* as request/response body advice instances.
|
||||
*/
|
||||
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters,
|
||||
ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) {
|
||||
|
||||
super(converters, requestResponseBodyAdvice);
|
||||
this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager());
|
||||
this.safeExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions());
|
||||
this.safeExtensions.addAll(WHITELISTED_EXTENSIONS);
|
||||
}
|
||||
|
||||
|
||||
@@ -164,6 +198,7 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
|
||||
inputMessage, outputMessage);
|
||||
if (returnValue != null) {
|
||||
addContentDispositionHeader(inputMessage, outputMessage);
|
||||
((GenericHttpMessageConverter<T>) messageConverter).write(returnValue,
|
||||
returnValueType, selectedMediaType, outputMessage);
|
||||
if (logger.isDebugEnabled()) {
|
||||
@@ -179,6 +214,7 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
|
||||
inputMessage, outputMessage);
|
||||
if (returnValue != null) {
|
||||
addContentDispositionHeader(inputMessage, outputMessage);
|
||||
((HttpMessageConverter<T>) messageConverter).write(returnValue,
|
||||
selectedMediaType, outputMessage);
|
||||
if (logger.isDebugEnabled()) {
|
||||
@@ -225,7 +261,7 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
/**
|
||||
* @see #getProducibleMediaTypes(HttpServletRequest, Class, Type)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@SuppressWarnings({"unchecked", "unused"})
|
||||
protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> returnValueClass) {
|
||||
return getProducibleMediaTypes(request, returnValueClass, null);
|
||||
}
|
||||
@@ -278,4 +314,48 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
|
||||
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(HttpHeaders.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(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=f.txt");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSafeExtension(String extension) {
|
||||
return (!StringUtils.hasText(extension) ||
|
||||
this.safeExtensions.contains(extension.toLowerCase(Locale.ENGLISH)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@@ -70,6 +71,12 @@ public class MappingJackson2JsonView extends AbstractJackson2View {
|
||||
*/
|
||||
public static final String DEFAULT_JSONP_CONTENT_TYPE = "application/javascript";
|
||||
|
||||
/**
|
||||
* Pattern for validating jsonp callback parameter values.
|
||||
*/
|
||||
private static final Pattern CALLBACK_PARAM_PATTERN = Pattern.compile("[0-9A-Za-z_\\.]*");
|
||||
|
||||
|
||||
private String jsonPrefix;
|
||||
|
||||
private Set<String> modelKeys;
|
||||
@@ -170,14 +177,32 @@ public class MappingJackson2JsonView extends AbstractJackson2View {
|
||||
if (this.jsonpParameterNames != null) {
|
||||
for (String name : this.jsonpParameterNames) {
|
||||
String value = request.getParameter(name);
|
||||
if (!StringUtils.isEmpty(value)) {
|
||||
return value;
|
||||
if (StringUtils.isEmpty(value)) {
|
||||
continue;
|
||||
}
|
||||
if (!isValidJsonpQueryParam(value)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Ignoring invalid jsonp parameter value: " + value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out undesired attributes from the given model.
|
||||
* The return value can be either another {@link Map} or a single value object.
|
||||
@@ -228,6 +253,7 @@ public class MappingJackson2JsonView extends AbstractJackson2View {
|
||||
jsonpFunction = ((MappingJacksonValue) object).getJsonpFunction();
|
||||
}
|
||||
if (jsonpFunction != null) {
|
||||
generator.writeRaw("/**/");
|
||||
generator.writeRaw(jsonpFunction + "(" );
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user