Protect against RFD exploits

Issue: SPR-13548
This commit is contained in:
Rossen Stoyanchev
2015-10-04 21:25:41 -04:00
committed by Stephane Nicoll
parent 161fd98656
commit 2bd1daa75e
20 changed files with 465 additions and 109 deletions

View File

@@ -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".

View File

@@ -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)));
}
}

View File

@@ -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 + "(" );
}
}

View File

@@ -19,6 +19,7 @@ package org.springframework.web.servlet.mvc.method.annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -53,7 +54,8 @@ import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.ModelAndView;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* Unit tests for {@link RequestMappingHandlerAdapter}.
@@ -128,7 +130,7 @@ public class RequestMappingHandlerAdapterTests {
HandlerMethodReturnValueHandler viewHandler = new ViewNameMethodReturnValueHandler();
this.handlerAdapter.setArgumentResolvers(Arrays.asList(redirectAttributesResolver, modelResolver));
this.handlerAdapter.setReturnValueHandlers(Arrays.asList(viewHandler));
this.handlerAdapter.setReturnValueHandlers(Collections.singletonList(viewHandler));
this.handlerAdapter.setIgnoreDefaultModelOnRedirect(true);
this.handlerAdapter.afterPropertiesSet();
@@ -143,7 +145,7 @@ public class RequestMappingHandlerAdapterTests {
@Test
public void setCustomArgumentResolvers() throws Exception {
HandlerMethodArgumentResolver resolver = new ServletRequestMethodArgumentResolver();
this.handlerAdapter.setCustomArgumentResolvers(Arrays.asList(resolver));
this.handlerAdapter.setCustomArgumentResolvers(Collections.singletonList(resolver));
this.handlerAdapter.afterPropertiesSet();
assertTrue(this.handlerAdapter.getArgumentResolvers().contains(resolver));
@@ -153,7 +155,7 @@ public class RequestMappingHandlerAdapterTests {
@Test
public void setArgumentResolvers() throws Exception {
HandlerMethodArgumentResolver resolver = new ServletRequestMethodArgumentResolver();
this.handlerAdapter.setArgumentResolvers(Arrays.asList(resolver));
this.handlerAdapter.setArgumentResolvers(Collections.singletonList(resolver));
this.handlerAdapter.afterPropertiesSet();
assertMethodProcessorCount(1, INIT_BINDER_RESOLVER_COUNT, HANDLER_COUNT);
@@ -162,7 +164,7 @@ public class RequestMappingHandlerAdapterTests {
@Test
public void setInitBinderArgumentResolvers() throws Exception {
HandlerMethodArgumentResolver resolver = new ServletRequestMethodArgumentResolver();
this.handlerAdapter.setInitBinderArgumentResolvers(Arrays.<HandlerMethodArgumentResolver>asList(resolver));
this.handlerAdapter.setInitBinderArgumentResolvers(Collections.singletonList(resolver));
this.handlerAdapter.afterPropertiesSet();
assertMethodProcessorCount(RESOLVER_COUNT, 1, HANDLER_COUNT);
@@ -171,7 +173,7 @@ public class RequestMappingHandlerAdapterTests {
@Test
public void setCustomReturnValueHandlers() {
HandlerMethodReturnValueHandler handler = new ViewNameMethodReturnValueHandler();
this.handlerAdapter.setCustomReturnValueHandlers(Arrays.asList(handler));
this.handlerAdapter.setCustomReturnValueHandlers(Collections.singletonList(handler));
this.handlerAdapter.afterPropertiesSet();
assertTrue(this.handlerAdapter.getReturnValueHandlers().contains(handler));
@@ -181,7 +183,7 @@ public class RequestMappingHandlerAdapterTests {
@Test
public void setReturnValueHandlers() {
HandlerMethodReturnValueHandler handler = new ModelMethodProcessor();
this.handlerAdapter.setReturnValueHandlers(Arrays.asList(handler));
this.handlerAdapter.setReturnValueHandlers(Collections.singletonList(handler));
this.handlerAdapter.afterPropertiesSet();
assertMethodProcessorCount(RESOLVER_COUNT, INIT_BINDER_RESOLVER_COUNT, 1);
@@ -240,20 +242,37 @@ public class RequestMappingHandlerAdapterTests {
this.handlerAdapter.setMessageConverters(converters);
this.webAppContext.registerSingleton("rba", ResponseCodeSuppressingAdvice.class);
this.webAppContext.registerSingleton("ja", JsonpAdvice.class);
this.webAppContext.refresh();
this.request.addHeader("Accept", MediaType.APPLICATION_JSON_VALUE);
this.request.setParameter("c", "callback");
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handleWithResponseEntity");
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handleBadRequest");
this.handlerAdapter.afterPropertiesSet();
this.handlerAdapter.handle(this.request, this.response, handlerMethod);
assertEquals(200, this.response.getStatus());
assertEquals("callback({\"status\":400,\"message\":\"body\"});", this.response.getContentAsString());
assertEquals("{\"status\":400,\"message\":\"body\"}", this.response.getContentAsString());
}
@Test
public void jsonpResponseBodyAdvice() throws Exception {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(new MappingJackson2HttpMessageConverter());
this.handlerAdapter.setMessageConverters(converters);
this.webAppContext.registerSingleton("jsonpAdvice", JsonpAdvice.class);
this.webAppContext.refresh();
testJsonp("callback", true);
testJsonp("_callback", true);
testJsonp("_Call.bAcK", true);
testJsonp("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_.", true);
testJsonp("<script>", false);
testJsonp("!foo!bar", false);
}
private HandlerMethod handlerMethod(Object handler, String methodName, Class<?>... paramTypes) throws Exception {
Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes);
@@ -266,6 +285,26 @@ public class RequestMappingHandlerAdapterTests {
assertEquals(handlerCount, this.handlerAdapter.getReturnValueHandlers().size());
}
private void testJsonp(String value, boolean validValue) throws Exception {
this.request = new MockHttpServletRequest("GET", "/");
this.request.addHeader("Accept", MediaType.APPLICATION_JSON_VALUE);
this.request.setParameter("c", value);
this.response = new MockHttpServletResponse();
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handleWithResponseEntity");
this.handlerAdapter.afterPropertiesSet();
this.handlerAdapter.handle(this.request, this.response, handlerMethod);
assertEquals(200, this.response.getStatus());
if (validValue) {
assertEquals("/**/" + value + "({\"foo\":\"bar\"});", this.response.getContentAsString());
}
else {
assertEquals("{\"foo\":\"bar\"}", this.response.getContentAsString());
}
}
@SuppressWarnings("unused")
private static class SimpleController {
@@ -279,9 +318,15 @@ public class RequestMappingHandlerAdapterTests {
return null;
}
public ResponseEntity<String> handleWithResponseEntity() {
public ResponseEntity<Map<String, String>> handleWithResponseEntity() {
return new ResponseEntity<Map<String, String>>(Collections.singletonMap(
"foo", "bar"), HttpStatus.OK);
}
public ResponseEntity<String> handleBadRequest() {
return new ResponseEntity<String>("body", HttpStatus.BAD_REQUEST);
}
}
@@ -355,12 +400,12 @@ public class RequestMappingHandlerAdapterTests {
}
}
@ControllerAdvice
private static class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
@ControllerAdvice
private static class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
public JsonpAdvice() {
super("c");
public JsonpAdvice() {
super("c");
}
}
}
}

View File

@@ -16,8 +16,6 @@
package org.springframework.web.servlet.mvc.method.annotation;
import static org.junit.Assert.*;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
@@ -56,6 +54,7 @@ import org.springframework.mock.web.test.MockHttpServletRequest;
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.annotation.RequestMapping;
@@ -68,6 +67,13 @@ import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
import org.springframework.web.util.WebUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
* Test fixture for a {@link RequestResponseBodyMethodProcessor} with
@@ -326,6 +332,37 @@ public class RequestResponseBodyMethodProcessorTests {
processor.writeWithMessageConverters(new ByteArrayOutputStream(), returnType, this.webRequest);
}
@Test
public void addContentDispositionHeader() throws Exception {
ContentNegotiationManagerFactoryBean factory = new ContentNegotiationManagerFactoryBean();
factory.addMediaType("pdf", new MediaType("application", "pdf"));
factory.afterPropertiesSet();
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
Collections.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);
}
@Test
public void supportsReturnTypeResponseBodyOnType() throws Exception {
Method method = ResponseBodyController.class.getMethod("handle");
@@ -598,6 +635,27 @@ public class RequestResponseBodyMethodProcessorTests {
assertTrue(content.contains("\"name\":\"bar\""));
}
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);
}
String handle(
@RequestBody List<SimpleBean> list,

View File

@@ -324,43 +324,14 @@ public class MappingJackson2JsonViewTests {
}
@Test
public void renderWithJsonpDefaultParameterName() throws Exception {
Map<String, Object> model = new HashMap<String, Object>();
model.put("foo", "bar");
request.addParameter("otherparam", "value");
request.addParameter("jsonp", "jsonpCallback");
public void renderWithJsonp() throws Exception {
testJsonp("jsonp", "callback", true);
testJsonp("jsonp", "_callback", true);
testJsonp("jsonp", "_Call.bAcK", true);
testJsonp("jsonp", "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_.", true);
view.render(model, request, response);
String content = response.getContentAsString();
assertEquals("jsonpCallback({\"foo\":\"bar\"});", content);
}
@Test
public void renderWithCallbackDefaultParameterName() throws Exception {
Map<String, Object> model = new HashMap<String, Object>();
model.put("foo", "bar");
request.addParameter("otherparam", "value");
request.addParameter("callback", "jsonpCallback");
view.render(model, request, response);
String content = response.getContentAsString();
assertEquals("jsonpCallback({\"foo\":\"bar\"});", content);
}
@Test
public void renderWithCustomJsonpParameterName() throws Exception {
Map<String, Object> model = new HashMap<String, Object>();
model.put("foo", "bar");
request.addParameter("otherparam", "value");
request.addParameter("custom", "jsonpCallback");
view.setJsonpParameterNames(new LinkedHashSet(Arrays.asList("jsonp", "callback", "custom")));
view.render(model, request, response);
String content = response.getContentAsString();
assertEquals("jsonpCallback({\"foo\":\"bar\"});", content);
testJsonp("jsonp", "<script>", false);
testJsonp("jsonp", "!foo!bar", false);
}
private void validateResult() throws Exception {
@@ -376,6 +347,26 @@ public class MappingJackson2JsonViewTests {
assertEquals("application/json", response.getContentType());
}
private void testJsonp(String paramName, String paramValue, boolean validValue) throws Exception {
Map<String, Object> model = new HashMap<String, Object>();
model.put("foo", "bar");
this.request = new MockHttpServletRequest();
this.request.addParameter("otherparam", "value");
this.request.addParameter(paramName, paramValue);
this.response = new MockHttpServletResponse();
this.view.render(model, this.request, this.response);
String content = this.response.getContentAsString();
if (validValue) {
assertEquals("/**/" + paramValue + "({\"foo\":\"bar\"});", content);
}
else {
assertEquals("{\"foo\":\"bar\"}", content);
}
}
public interface MyJacksonView1 {
}