SpringMVC如何设置Response的Content-Type
以2.7.18版本为例,看看在SpringMVC中是如何设置响应的Content-Type的:
在SpringMvc中,DispatcherServlet是前端控制器设计模式的实现,提供Spring Web MVC的集中访问点,而且负责职责的分派。在这里下断点。通过反复调试,最后发现org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters方法里完成Content-Type的配置。
一开始会根据返回值类型进行相应的逻辑处理,若返回值是个org.springframework.core.io.Resource为资源类型的话会进行如下处理:
下面就是对contentType的处理了,selectedMediaType表示最终被选中的MediaType,请求是可以接受多种MediaType的,首先会获取请求方指定的contentType,如果请求方法指定了,就以它的为准否则就走else逻辑寻找到一个最为合适的:
acceptableTypes是浏览器可以接收的数据类型,也即从请求头里的Accept解析出的内容。首先调用getAcceptableMediaTypes方法会从request 中获取可以接受的媒体类型:
查看具体的解析方法,主要是从请求头里的Accept解析获取对应的内容:
然后是调用getProducibleMediaTypes方法获得服务器可以输出的数据类型:
这里实际上是遍历每个HttpMessageConverter,判断其是否支持写入,如果支持写入的话,便将其支持的mediaType拿到,全部支持写入的HttpMessageConverter对应的mediaType集合就是服务器支持的medisType:
默认情况下主要是以下HttpMessageConverter,例如ByteArrayHttpMessageConverter是用来解析byte数组的,StringHttpMessageConverter是用来解析字符串的,MappingJackson2HttpMessageConverter对象是用来转化JSON数据的:
常见的HttpMessageConverter以及支持的MediaType类型如下:
如果返回的body不为null并且获取不到producibleMediaTypes的话会抛出异常。
接着判断producibleTypes与acceptableTypes是否兼容, 如果兼容, 将更明确的类型添加到mediaTypesToUse中(是producibleTypes和acceptableType的交集):
如果最终找不到可用mediaType则按异常处理:
然后按照权重排序,遍历mediaTypesToUse并从中确定一个输出的数据类型:
到这里已经得到了Http确定的Content-Type输出类型后,需要将请求返回值转化。根据MediaType类型匹配到对应的HttpMessageConverter 消息转换器后处理 body 数据后即可返回:
到这里SpringMVC已经完成了Response的Content-Type的设置,响应头的Content-Type主要是由请求头的Accept决定的.
看一个具体的例子映证猜想:
"/health"}) (value = {
public String health(String msg) {
return msg;
}
默认情况下不传输Accept头部的话,response的Content-Type为text/plain;charset=UTF-8:
按照前面的分析,通过调整Accept头部值,即可调整response的Content-Type内容了:
潜在XSS风险 @ResponseBody 注解的作用是将Controller的方法返回的对象,通过适当的转换器转换为指定的格式之后,写入到response对象的body,通常用来返回JSON数据或者是XML数据。
例如下面的例子:
"/showMsg"}) (value = {
public MsgInfo showMsg(String msg) {
return new MsgInfo(msg);
}
正常访问showMsg接口,可以看到将response返回了特定的JSON格式:
因为Response的Content-type被设置成了application/json,一般情况下即使msg参数写入了恶意的xss语句,通过浏览器解析后也不会触发:
按照前面的猜想,因为Spring应用中响应头的Content-Type主要是由请求头的Accept决定的。那假设Accept头设置为text/html即可触发呢,答案是否定的,可以看到虽然Response的Content-Type虽然成功设置成了text/html,但是却返回了406 status,并没有正常解析请求的返回内容:
主要原因如下:
按照前面的解析逻辑,首先调用getAcceptableMediaTypes方法会从request 中获取可以接受的媒体类型,这里是设置好的text/html;charset=UTF-8:
然后调用getProducibleMediaTypes方法获得服务器可以输出的数据类型,然后遍历每个HttpMessageConverter,判断其是否支持写入,如果支持写入的话,便将其支持的mediaType拿到,这里会通过MappingJackson2HttpMessageConverter处理,其默认支持的mediaType为application/json:
所以最终获取到的支持mediaType如下:
然后尝试producibleTypes和acceptableType的交集,找到后更明确的类型添加到mediaTypesToUse中,因为这里text/html明显与application/json没有交集,所以最终mediaTypesToUse集合的size为0,当response的body不为null时,会抛出对应的异常,也就是最终返回的406 status:
这里很多研发会有一个误区,对于这种场景,即使没有对用户输入进行处理,也没办法触发xss。
实际上这么理解是并不正确。那么对于常见的返回为JavaBean的场景,是否就不需要考虑xss风险呢?
看一个实际的例子,Springboot 默认的json处理方式Jackson,若我们想使用Fastjson进行解析时,需要进行额外的配置,下面是在网上找的一个案例代码:
@Configuration
public class FastjonConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//创建fastJson消息转换器
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
List<MediaType> supportedMediaTypes = new ArrayList<>();
supportedMediaTypes.add(MediaType.APPLICATION_JSON);
supportedMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML);
supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
supportedMediaTypes.add(MediaType.APPLICATION_PDF);
supportedMediaTypes.add(MediaType.APPLICATION_RSS_XML);
supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML);
supportedMediaTypes.add(MediaType.APPLICATION_XML);
supportedMediaTypes.add(MediaType.IMAGE_GIF);
supportedMediaTypes.add(MediaType.IMAGE_JPEG);
supportedMediaTypes.add(MediaType.IMAGE_PNG);
supportedMediaTypes.add(MediaType.TEXT_EVENT_STREAM);
supportedMediaTypes.add(MediaType.TEXT_HTML);
supportedMediaTypes.add(MediaType.TEXT_MARKDOWN);
supportedMediaTypes.add(MediaType.TEXT_PLAIN);
supportedMediaTypes.add(MediaType.TEXT_XML);
fastConverter.setSupportedMediaTypes(supportedMediaTypes);
//创建配置类
FastJsonConfig fastJsonConfig = new FastJsonConfig();
//修改配置返回内容的过滤
//WriteNullListAsEmpty :List字段如果为null,输出为[],而非null
//WriteNullStringAsEmpty :字符类型字段如果为null,输出为"",而非null
//DisableCircularReferenceDetect :消除对同一对象循环引用的问题,默认为false(如果不配置有可能会进入死循环)
//WriteNullBooleanAsFalse:Boolean字段如果为null,输出为false,而非null
//WriteMapNullValue:是否输出值为null的字段,默认为false
fastJsonConfig.setSerializerFeatures(
SerializerFeature.DisableCircularReferenceDetect,
SerializerFeature.WriteMapNullValue,
SerializerFeature.WriteNullListAsEmpty,
SerializerFeature.WriteNullStringAsEmpty,
SerializerFeature.WriteNullBooleanAsFalse
);
fastConverter.setFastJsonConfig(fastJsonConfig);
//将fastjson添加到视图消息转换器列表内
converters.add(fastConverter);
}
}
通过配置@Configuration后,此时Spring会使用Fastjson进行解析。同样是刚刚showMsg接口的例子,当设置Accept后,此时返回response的Content-Type内容为text/html:
根据浏览器默认的Accept,也就是说在firefox直接访问是可以触发xss的:
具体原因其实是因为在配置Fastjson解析时,额外增加了supportedMediaTypes,这些值都会添加到producibleTypes中:
然后在尝试producibleTypes和acceptableType的交集,找到后更明确的类型添加到mediaTypesToUse时,可以看到mediaTypesToUse并不像之前那样size为0,而是为text/html:
当请求msg为恶意xss语句时,成功触发了xss。
所以在日常审计时,需要额外关注这类场景,避免不必要的风险引入。当然了,用户一切输入都是不可信的,规范用户输入,不依赖框架本身的机制才是最保险的做法。
往期推荐
原文始发于微信公众号(SecIN技术平台):原创 | 浅谈SpringMvc响应Content-Type与xss