vlambda博客
学习文章列表

【硬核】由XSS问题探索一下SpringMVC和FastJson

前言

好久没来写文章了,所以在某人的催促之下,把最近的一个需求进行总结一下。在这个需求的开发过程中,还是有不少收获的,所以做一下分享,如有补充与不正之处,欢迎大家的批评与斧正。

背景

我们先来讲一下这个需求的背景吧。看到这个标题,可以想到是不是因为系统遭遇了XSS攻击所以需要进行过滤。没错,这是因为前几天有一个憨憨提交了一段含有脚本的内容,结果成功保存进去,然后在展示的时候,浏览器直接alert出来这个提示(传统项目中的常见漏洞,见谅见谅),这个也是比较常见的XSS攻击的场景。为此,需要对XSS的内容进行过滤,以及对系统内的脏数据进行兼容处理(后来在系统里仔细搜了下,还确实有不少XSS的内容,估计是友商的友善操作)。【硬核】由XSS问题探索一下SpringMVC和FastJson

需求分析

看到这里,估计有不少小伙伴嗤之以鼻,心里在想:这个东西对我来说实在是太小儿科了。于是百度一下,啪的一声就把代码甩在我的脸上。这些答案,大致上是这样的:提供一个Xss的Filter,然后在这个Filter里面对Request进行RequestWrapper的封装,并且重写里面的getParameter方法和getParameterValues方法,然后对参数值进行Xss数据清洗。代码如下:

public class XssFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Assert.notNull(filterConfig, "FilterConfig must not be null");
        {
            // 普通代码块
            String temp = filterConfig.getInitParameter(PARAM_NAME_EXCLUSIONS);
            String[] url = StringUtils.split(temp, DEFAULT_SEPARATOR_CHARS);
            for (int i = 0; url != null && i < url.length; i++) {
                excludes.add(url[i]);
            }
        }
        log.info("WebFilter->[{}] init success...", filterConfig.getFilterName());
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException 
{
        if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
            HttpServletRequest req = (HttpServletRequest) request;
            HttpServletResponse resp = (HttpServletResponse) response;
            if (handleExcludeUrl(req, resp)) {
                chain.doFilter(request, response);
                return;
            }
            // 对HttpServletRequest进行包装
            XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request);
            chain.doFilter(xssRequest, response);
        } else {
            chain.doFilter(request, response);
        }
    }

    /**
     * 当前请求是否是排除的链接
     *
     * @return boolean
     */

    private boolean handleExcludeUrl(HttpServletRequest request, HttpServletResponse response) {
        Assert.notNull(request, "HttpServletRequest must not be null");
        Assert.notNull(response, "HttpServletResponse must not be null");
        if (ObjectUtils.isEmpty(excludes)) {
            return false;
        }
        // 返回除去host(域名或者ip)部分的路径
        String requestUri = request.getRequestURI();
        // 返回除去host和工程名部分的路径
        String servletPath = request.getServletPath();
        // 返回全路径
        StringBuffer requestURL = request.getRequestURL();
        for (String pattern : excludes) {
            Pattern p = Pattern.compile("^" + pattern);
            Matcher m = p.matcher(requestURL);
            if (m.find()) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void destroy() {
    }

    /**
     * 排除链接默认分隔符
     */

    public static final String DEFAULT_SEPARATOR_CHARS = ",";

    /**
     * 需要忽略排除的链接参数名
     */

    public static final String PARAM_NAME_EXCLUSIONS = "exclusions";

    /**
     * 需要忽略排除的链接参数值
     * http://localhost,http://127.0.0.1,
     */

    public static final String PARAM_VALUE_EXCLUSIONS = "";

    /**
     * 排除链接
     */

    public List<String> excludes = Lists.newArrayList();

}

XssHttpServletRequestWrapper的代码如下

import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.safety.Whitelist;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
import java.util.Enumeration;

/**
 * XSS过滤处理
 *
 */

public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {

    /**
     * 默认XSS过滤处理白名单
     */

    public final static Whitelist DEFAULT_WHITE_LIST = Whitelist.relaxed().addAttributes(":all""style");

    /**
     * 唯一构造器
     *
     * @param request HttpServletRequest
     */

    public XssHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String getHeader(String name) {
        return super.getHeader(name);
    }

    @Override
    public Enumeration<String> getHeaders(String name) {
        return super.getHeaders(name);
    }

    @Override
    public String getRequestURI() {
        return super.getRequestURI();
    }

    /**
     * 需要手动调用此方法,SpringMVC默认应该使用getParameterValues来封装的Model
     */

    @Override
    public String getParameter(String name) {
        String parameter = super.getParameter(name);
        if (parameter != null) {
            return cleanXSS(parameter);
        }
        return null;
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] values = super.getParameterValues(name);
        if (values != null) {
            int length = values.length;
            String[] escapeValues = new String[length];
            for (int i = 0; i < length; i++) {
                if (null != values[i]) {
                    // 防xss攻击和过滤前后空格
                    escapeValues[i] = cleanXSS(values[i]);
                } else {
                    escapeValues[i] = null;
                }
            }
            return escapeValues;
        }
        return null;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return super.getInputStream();
    }
    

    public static String cleanXSS(String value) {
        if (value != null) {
            value = StringUtils.trim(Jsoup.clean(value, DEFAULT_WHITE_LIST));
        }
        return value;
    }
}

当然,这里我就使用了Jsoup作为XSS内容清除的工具,具体的用法,各位可以自行百度。
【硬核】由XSS问题探索一下SpringMVC和FastJson
我想以上应该是各位比较常见的写法。这样的写法当然没啥问题,但是其实有一个局限性,那就是这种方式只适合form表单提交的请求,而如果是那种JSON数据请求的,就获取不到数据,而我们系统大多数的接口请求都是JSON数据提交的。
此外还有一个问题,那就是我们系统里面已经有XSS脏数据了,那么要解决这个问题,也是两个角度出发:

  1. 处理系统当中的脏数据,也就是SQL清洗. 2.在数据返回的时候进行数据清洗.

对于第一种方案,由于系统是分库分表,而且期间的业务表冗余庞大,SQL的清除工作会显得十分困难,而且存在巨大的风险与隐患。所以我们这边还是采用第二种方案,由于数据都是采用JSON数据返回,那么只需要做一个通用的转换,进行数据清洗即可。
那么我们对需要做的事情进行数据总结一下:

  1. 对于JSON数据请求进行数据清洗
  2. 对于JSON数据返回进行数据清洗

源码探索

按照我以往设计解决方案的时候,我都会先从执行流程以及源码等方面来看这些问题。所以我想对于JSON的请求和返回的过程进行的探索可能会有所启示。目前大多数系统都是基于SpringMVC作为前置框架,所以以下也是基于SpringMVC的源码进行解析,spring-webmvc的版本是5.2.9.RELEASE,其他版本的代码应该大致相同,就不做过多细究。在JSON数据上我们使用的是fastjson(虽然fastjson在稳定性等方面还是不如jackson,但是我希望我们国产的能够越来越好,还是要支持一波),使用的版本是1.2.72。
【硬核】由XSS问题探索一下SpringMVC和FastJson

JSON数据返回

首先我们还是先从JSON数据返回开始着手,这个也是因为,即便无法完成对JSON数据请求的数据清洗,能够从JSON数据返回当中解决,进行一个兜底的方案处理,至少还是一个让人安心的事情。
SpringMVC在扫描到@ResponseBody之后,就知道这个接口是用来返回JSON数据的。在spring配置文件中,我们配置了如下:

<bean id="fastJsonHttpMessageConverter"
      class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">

    <property name="supportedMediaTypes">
        <list>
            <value>text/html;charset=UTF-8</value>
            <value>application/json;charset=UTF-8</value>
        </list>
    </property>
    <property name="features">
        <array>
            <value>DisableCircularReferenceDetect</value>
        </array>
    </property>
</bean>

<mvc:annotation-driven>
    <mvc:message-converters>
        <ref bean="fastJsonHttpMessageConverter"/>
    </mvc:message-converters>
</mvc:annotation-driven>

当然各位也可以使用注解的方式

@Configuration
public class SpringWebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
        FastJsonConfig config = new FastJsonConfig();
        config.setSerializerFeatures(
                // 避免循环引用
                SerializerFeature.DisableCircularReferenceDetect);
        converter.setFastJsonConfig(config);
        converter.setDefaultCharset(Charset.forName("UTF-8"));
        List<MediaType> mediaTypeList = new ArrayList<>();
        mediaTypeList.add(MediaType.APPLICATION_JSON);
        converter.setSupportedMediaTypes(mediaTypeList);
        // 为什么不是直接add,这个在下文中会提到
        converters.set(0, converter);
    }

}

两种方式其实都是将自定义的HttpMessageConverter注入到Spring内部当中的messageConverts当中,效果是一样的。

DispatcherServlet

对于这个类,我想我们应该是再熟悉不过了,即便没有看过源码。各位在刚学SpringMVC的时候,各类教程里面都是告诉你需要在web.xml配置文件当中配置这个类。这是因为DispatcherServlet这个类是作为SpringMVC所有的请求的一个入口类。当Tomcat将请求包装之后流转到HttpServlet当中,这个时候的HttpServlet实际上是Spring提供的FrameworkServlet,紧接着进入的doService方法则是由FrameworkServlet,也就是DispatcherServlet所实现。如图所示:【硬核】由XSS问题探索一下SpringMVC和FastJson那么在这个doDispatch方法里面,经过一层层调用,就会调用到HandlerMethodReturnValueHandlerComposite#handleReturnValue(中间的环节我就略过了,各位也可以自行DEBUG)

 @Override
 public void handleReturnValue(Object returnValue, MethodParameter returnType,
   ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
 throws Exception 
{

  HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
  if (handler == null) {
   throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
  }
  handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
 }

HandlerMethodReturnValueHandlerComposite

这里有一个selectHandler私有方法:

 private HandlerMethodReturnValueHandler selectHandler(Object value, MethodParameter returnType) {
  boolean isAsyncValue = isAsyncReturnValue(value, returnType);
  for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
   if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
    continue;
   }
   if (handler.supportsReturnType(returnType)) {
    return handler;
   }
  }
  return null;
 }

SpringMVC里面内置了15个HandlerMethodReturnValueHandler,每一个HandlerMethodReturnValueHandler都实现了supportsReturnType。通过这个接口我们来确定在运行的时候是用哪一个处理器。这里我们来看下RequestResponseBodyMethodProcessor所实现的supportsReturnType方法:

 @Override
 public boolean supportsReturnType(MethodParameter returnType) {
  return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
    returnType.hasMethodAnnotation(ResponseBody.class))
;
 }

可以看到,这里还是围绕着@ResponseBody这个注解来的。这下,以后面试官要是问你,为什么@ResponseBody是用来返回JSON数据的,那么,看到这个步骤,我们大概清楚,因为这个注解的存在,所以我们所使用的HandlerMethodReturnValueHandler选择的是RequestResponseBodyMethodProcessor。
既然确定了具体的处理器,那么我们就进入到它的handleReturnValue方法。

RequestResponseBodyMethodProcessor

直接先看源码

 @Override
 public void handleReturnValue(Object returnValue, MethodParameter returnType,
   ModelAndViewContainer mavContainer, NativeWebRequest webRequest)

   throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException 
{

  mavContainer.setRequestHandled(true);
  ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
  ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

  // Try even with null return value. ResponseBodyAdvice could get involved.
  writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
 }

这里其实也没什么好说的,就是直接调用了它所继承的抽象方法。

AbstractMessageConverterMethodProcessor

进入到writeWithMessageConverters这个方法,我看来看到这其中的这一部分

if (selectedMediaType != null) {
    selectedMediaType = selectedMediaType.removeQualityValue();
    for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
        if (messageConverter instanceof GenericHttpMessageConverter) {
            if (((GenericHttpMessageConverter) messageConverter).canWrite(
                    declaredType, valueType, selectedMediaType)) {
                // 写入之前执行动作
                outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
                        (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
                        inputMessage, outputMessage);
                if (outputValue != null) {
                    addContentDispositionHeader(inputMessage, outputMessage);
                    // 具体的messageConverter进行写入
                    ((GenericHttpMessageConverter) messageConverter).write(
                            outputValue, declaredType, selectedMediaType, outputMessage);
                    if (logger.isDebugEnabled()) {
                        logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
                                "\" using [" + messageConverter + "]");
                    }
                }
                return;
            }
        }
        else if (messageConverter.canWrite(valueType, selectedMediaType)) {
            outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
                    (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
                    inputMessage, outputMessage);
            if (outputValue != null) {
                addContentDispositionHeader(inputMessage, outputMessage);
                ((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage);
                if (logger.isDebugEnabled()) {
                    logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
                            "\" using [" + messageConverter + "]");
                }
            }
            return;
        }
    }
}

看上去这段代码好像很麻烦,但是实际上关键的就两个步骤:

  1. getAdvice().beforeBodyWrite()
  2. write() 没错,write方法就是我们返回JSON数据写入IO流,而getAdvice().beforeBodyWrite()就是在写入之前可以对数据进行处理的前置方法,也是Spring给开发者留下的钩子。所以这就可能是一个可以考虑的地方,在下面还会提到,我们继续往下看。

FastJsonHttpMessageConverter

我们继续回到上面的for循环当中,看看这个this.messageConverters【硬核】由XSS问题探索一下SpringMVC和FastJson

为什么是FastJsonHttpMessageConverter

我们可以看到,SpringMVC内置了很多HttpMessageConverter。this.messageConverters是在Spring容器启动的时候就会进行加载设置,先加载系统内置的,最后才会加载我们自定义的,也就是我们的主角FastJsonHttpMessageConverter。这些知道为什么我要写成

converters.set(0, converter);

目的就是为了在遍历的时候首先使用FastJsonHttpMessageConverter,不然默认执行的就是MappingJackson2HttpMessageConverter。(希望有朝一日,FastJsonHttpMessageConverter也能成为内置的转换器)

快说说this.messageConverters是怎么加载的

好吧,上面我一笔带过这个流程,想偷个懒,可还是被你们的好奇心发现了。上面说到,this.messageConverters是在Spring容器启动的时候就会进行加载设置,先加载系统内置的,最后才会加载我们自定义的。那它是怎么加载的呢?
我们先来DUBUG一下,从结果往前推。看看这里的堆栈

【硬核】由XSS问题探索一下SpringMVC和FastJson

可以发现传入的converters这个列表里面已经有了内置的10个转换器了,那到这里就说明是先赋值系统内置的转换器,然后再轮到我们自定义的。
额,这样的解释我猜你们显然是不尽兴的,那么我们继续往前推,看看是哪里的程序进入到这里: 【硬核】由XSS问题探索一下SpringMVC和FastJson我想看到这里,总该明白,这个this.delegates列表里面有两个WebMvcConfigurer的实现类,按照顺序进行遍历。可以看到我们自定义的WebMvcConfigurer是晚于Spring内置的WebMvcAutoConfigurationAdapter。
额,不是吧,竟然还会问为什么this.delegates里面的顺序是这样的? 【硬核】由XSS问题探索一下SpringMVC和FastJson
那么我们看下上面是怎么给this.delegates进行赋值的 【硬核】由XSS问题探索一下SpringMVC和FastJson可以看到这个WebMvcConfigurer也是由外部传入的,我们看下是哪里传入的 【硬核】由XSS问题探索一下SpringMVC和FastJson我想这个写法就应该很明白了,根据Autowired将容器中的WebMvcConfigurer赋值到这个configurers当中,至于赋值过程中的顺序,则会参考是不是有@Order注解,我们可以看到Spring给我们提供的WebMvcAutoConfigurationAdapter上面写了@Order(0)。所以各位老板要是不想使用


converters.set(0, converter);

这种写法而是直接add,那么可以在自定义的WebMvcConfigurer将优先级早于WebMvcAutoConfigurationAdapter也是可以的。【硬核】由XSS问题探索一下SpringMVC和FastJson

write做了什么事情呢

既然上面已经解释了为什么是FastJsonHttpMessageConverter,那么接下来我们就可以来看它的write方法。

@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {

    ByteArrayOutputStream outnew = new ByteArrayOutputStream();
    try {
        
        // ... 省略
        int len = JSON.writeJSONString(outnew, //
                fastJsonConfig.getCharset(), //
                value, //
                fastJsonConfig.getSerializeConfig(), //
                //fastJsonConfig.getSerializeFilters(), //
                allFilters.toArray(new SerializeFilter[allFilters.size()]),
                fastJsonConfig.getDateFormat(), //
                JSON.DEFAULT_GENERATE_FEATURE, //
                fastJsonConfig.getSerializerFeatures());
                
       // ... 省略
    } catch (JSONException ex) {
        throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
    } finally {
        outnew.close();
    }
}


省略一些中间环节,最后它就是进行核心的JSON化操作。接下来就是看Fastjson在这个过程中是否给我们预留了上面类似的钩子。答应是肯定的,毕竟也是这么优秀的开源项目。直接上代码吧

public class JavaBeanSerializer extends SerializeFilterable implements ObjectSerializer {

    protected void write(JSONSerializer serializer, //
                          Object object, //
                          Object fieldName, //
                          Type fieldType, //
                          int features,
                          boolean unwrapped
        )
 throws IOException 
{
    
     // 这里就是处理每一个key-value的过程
     Object originalValue = propertyValue;
  propertyValue = this.processValue(serializer, fieldSerializer.fieldContext, object, fieldInfoName, propertyValue, features);
    
    }
    
    protected Object processValue(JSONSerializer jsonBeanDeser, //
                           BeanContext beanContext,
                           Object object, //
                           String key, //
                           Object propertyValue, //
                           int features)
 
{
                           
                           
        if (jsonBeanDeser.valueFilters != null) {
            for (ValueFilter valueFilter : jsonBeanDeser.valueFilters) {
                propertyValue = valueFilter.process(object, key, propertyValue);
            }
        }                      
    }

}

在Fastjson处理每一个key-value的时候,都会去执行这个valueFilters。所以我们只需要在配置FastJsonHttpMessageConverter的时候,带上我们自定义的ValueFilter,这样它就会进行处理。我们来看下这个接口

public interface ValueFilter extends SerializeFilter {

    Object process(Object object, String name, Object value);
}

我们可以看到这个接口里面有一个process方法,所以我们也可以利用这个钩子,在输出JSON数据之前,对内容进行清除修改。

那么怎么选择呢

那么综合来看我们可以发现,整个流程当中会提到两个钩子

  1. SpringMVC写入之前的钩子
  2. SpringMVC使用的Fastjson在写入JSON之前的钩子 那各位觉得应该要使用哪一个钩子呢?我个人觉得,是使用Fastjson的钩子。因为假如是用SpringMVC提供的钩子,当拿到对象之后,大概率是会使用反射的方式进行修改内容,而后续的序列化,不管是Fastjson还是Jackson也是会用到反射。这两轮的反射会对性能造成一定程度的影响,所以与其都要修改,不如就留在Fastjson的钩子中去处理。
    【硬核】由XSS问题探索一下SpringMVC和FastJson

JSON数据请求

说完了返回的这一层面,我们再来看看JSON数据请求的这一情况,能不能也有相关的解决方案。既然说到请求,那么我们还是要回归到上面我们讲的DispatcherServlet。

DispatcherServlet

又回到这个类,这里我们就从doService入手,紧接着调用了doDispatch方法。(什么,你连为什么会走入doService都不知道,我……,出门左转找下Tomcat源码攻略)

/**
 * Exposes the DispatcherServlet-specific request attributes and delegates to {@link #doDispatch}
 * for the actual dispatching.
 */

@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
 
 // 一些配置操作,这里忽略
 try {
  doDispatch(request, response);
 }
 finally {
  if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
   // Restore the original attribute snapshot, in case of an include.
   if (attributesSnapshot != null) {
    restoreAttributesAfterInclude(request, attributesSnapshot);
   }
  }
 }
}

在这个doDispatch方法里面呢,会去寻找对应的HandlerAdapter,也就是RequestMappingHandlerAdapter这个适配器。【硬核】由XSS问题探索一下SpringMVC和FastJson也就是上面图片中的第二个小方框。至于第一个,就是大家经常背的面试题,问SpringMVC请求处理流程,其中获取接口处理器就是在这个地方。当然这里只是提一下。
那么是怎么决定是RequestMappingHandlerAdapter呢,我们看下这个getHandlerAdapter方法【硬核】由XSS问题探索一下SpringMVC和FastJson可以看到,Spring容器内提供了4个内置的handlerAdapters。RequestMappingHandlerAdapter并没有重写这个supports方法,所以使用的是抽象方法AbstractHandlerMethodAdapter当中的supports方法

 /**
  * This implementation expects the handler to be an {@link HandlerMethod}.
  * @param handler the handler instance to check
  * @return whether or not this adapter can adapt the given handler
  */

 @Override
 public final boolean supports(Object handler) {
  return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
 }

RequestMappingHandlerAdapter重写了supportsInternal方法,恒等于true

/**
 * Always return {@code true} since any method argument and return value
 * type will be processed in some way. A method argument not recognized
 * by any HandlerMethodArgumentResolver is interpreted as a request parameter
 * if it is a simple type, or as a model attribute otherwise. A return value
 * not recognized by any HandlerMethodReturnValueHandler will be interpreted
 * as a model attribute.
 */

@Override
protected boolean supportsInternal(HandlerMethod handlerMethod) {
 return true;
}

大家也可以看看上面的英文注释,还是比较易懂的。
而前者,在封装的Http请求的时候本就是使用HandlerMethod作为其中的处理类。所以是结合这些因素就决定是RequestMappingHandlerAdapter作为适配器。

InvocableHandlerMethod

画风一转,我们来到了这个类。在确定RequestMappingHandlerAdapter作为处理类适配器之后,紧接着会进入一系列操作。其他操作我们在这里不做关心,因为我们关心的是对于参数部分的操作。【硬核】由XSS问题探索一下SpringMVC和FastJson这里的getMethodArgumentValues方法就是用来获取接口中的参数【硬核】由XSS问题探索一下SpringMVC和FastJson而具体的是由this.resolvers调用resolveArgument这个方法来处理

HandlerMethodArgumentResolverComposite

上面的this.resolvers就是这个类来着【硬核】由XSS问题探索一下SpringMVC和FastJson这里又需要进行getArgumentResolver来获取真正的参数处理器HandlerMethodArgumentResolver【硬核】由XSS问题探索一下SpringMVC和FastJson这里的缓存我就不多说了。系统里面提供了26个参数处理器。

RequestResponseBodyMethodProcessor

【硬核】由XSS问题探索一下SpringMVC和FastJson我们在平常的开发当中,如果前端页面传的是一个json数据的话,我们会在SpringMVC的参数对象前面加上一个@RequestBody。以前学的时候,包括做的时候,反正只要加上这个注解,就可以接收到JSON数据了。那么今天我们终于可以知道,也正是因为有了这个注解的规范,流程上会根据supportsParameter接口方法来定位到这个处理器。(划重点,划重点,划重点)【硬核】由XSS问题探索一下SpringMVC和FastJson进入到resolveArgument,我们又看到了熟悉的东西。没错,这里的readWithMessageConverters又用到了我们之前说的MessageConverters。进入这个方法,看看它的实现

@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
 Type targetType)
 throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException 
{

 // 之前的逻辑省略

 EmptyBodyCheckingHttpInputMessage message;
 try {
  message = new EmptyBodyCheckingHttpInputMessage(inputMessage);

  for (HttpMessageConverter<?> converter : this.messageConverters) {
   Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
   GenericHttpMessageConverter<?> genericConverter =
     (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
   if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
     (targetClass != null && converter.canRead(targetClass, contentType))) {
    if (message.hasBody()) {
     HttpInputMessage msgToUse =
       getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
     body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
       ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
     body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
    }
    else {
     body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
    }
    break;
   }
  }
 }
 catch (IOException ex) {
  throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
 }

 // 省略后置逻辑

 return body;
}

可以看到这个里面的写法,跟之前的写法如出一辙,只是相比于返回JSON数据,多了一步后置处理。
那么由此我们也可以推测这里也存在两个钩子:

  1. SpringMVC提供的前置处理或者后置处理钩子处理数据
  2. HttpMessageConverter提供的钩子处理

FastJsonHttpMessageConverter

又回到这个类。上面写到的是写入JSON,这里是从IO中读取JSON【硬核】由XSS问题探索一下SpringMVC和FastJson接下来就又回到我们平时使用的JSON.parseObject()方法了。这里提供了很多的配置,那么这里是否会有我们可能会用到的钩子呢。
这个答案是显然。跟着程序的执行,我们来到了Fastjson解析比较核心的部分【硬核】由XSS问题探索一下SpringMVC和FastJson这一部分是从IO流中解析成字符串,紧接着调用parseObject【硬核】由XSS问题探索一下SpringMVC和FastJson这里提供了三种类型的processer,我也可以理解为是三种过滤器。很可惜这三种过滤器只有在找不到匹配的属性的时候才会进行调用。
那看来从Fastjson身上寻找钩子的方案可能行不通(当然,可能存在其他我不知道的钩子,只是按照我现在看下来可能有点鸡肋)

getAdvice()

既然Fastjson这条路可能行不通,那么我们还是来看下SpringMVC提供的钩子吧。

HttpInputMessage msgToUse =
  getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
  ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);

这里的getAdvice()获取的类型是RequestResponseBodyAdviceChain,它是AbstractMessageConverterMethodArgumentResolver当中的属性,所以我们只要关注下它是如何被赋值的(总感觉又要被你们坑了)。
【硬核】由XSS问题探索一下SpringMVC和FastJson从源码来看,是在构造函数里面进行赋值的,那么接着看看是从哪里进行传入参数的

【硬核】由XSS问题探索一下SpringMVC和FastJson

根据DEBUG的堆栈情况,可以跟踪到这个,那么这里的this.requestResponseBodyAdvice是怎么赋值的呢?系统会给我们自动装载JsonViewRequestBodyAdvice和JsonViewResponseBodyAdvice,这里我就不进行展开,我们就来看下是否可以赋值我们自定义的钩子。接着往堆栈上走,我们可以看下这个afterPropertiesSet。 DEBUG这个时候是定在了第561行,这个地方就是装载系统提供的Advice。这个时候不知道大家是否留意到这个方法


// Do this first, it may add ResponseBody advice beans
initControllerAdviceCache();

尤其是上面的这个注释,似乎这个地方有我们想要的这个方法里面,首先是寻找系统中的ControllerAdviceBean,寻找的依据还是根据这个ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());

/**
 * Find beans annotated with {@link ControllerAdvice @ControllerAdvice} in the
 * given {@link ApplicationContext} and wrap them as {@code ControllerAdviceBean}
 * instances.
 * <p>As of Spring Framework 5.2, the {@code ControllerAdviceBean} instances
 * in the returned list are sorted using {@link OrderComparator#sort(List)}.
 * @see #getOrder()
 * @see OrderComparator
 * @see Ordered
 */

public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {
 List<ControllerAdviceBean> adviceBeans = new ArrayList<>();
 for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class)) {
  if (!ScopedProxyUtils.isScopedTarget(name)) {
   ControllerAdvice controllerAdvice = context.findAnnotationOnBean(name, ControllerAdvice.class);
   if (controllerAdvice != null) {
    // Use the @ControllerAdvice annotation found by findAnnotationOnBean()
    // in order to avoid a subsequent lookup of the same annotation.
    adviceBeans.add(new ControllerAdviceBean(name, context, controllerAdvice));
   }
  }
 }
 OrderComparator.sort(adviceBeans);
 return adviceBeans;
}

可以看到这里是说,主要是根据@ControllerAdvice这个注解来定位到这个类,并且封装成ControllerAdviceBean对象。
我们再回到上面那个图,如果是实现了RequestBodyAdvice或者ResponseBodyAdvice接口的类,就会加入到requestResponseBodyAdviceBeans这个列表当中,并且会将这个Advice复制到我们上面说的this.requestResponseBodyAdvice,并且,是放在首位!可见,如果我们实现了自定义的Advice,Spring是希望优先执行我们的Advice。此外,我们可以看到这里也有ResponseBodyAdvice,没错,在上面没有选用的JSON数据返回所涉及到的钩子,也是在这个地方进行赋值的。
为此,我们可以简单实现下这一Advice

@RestControllerAdvice
public class XssRequestControllerAdvice implements RequestBodyAdvice {

    /**
     * 默认XSS过滤处理白名单
     */

    public final static Whitelist DEFAULT_WHITE_LIST = Whitelist.relaxed().addAttributes(":all""style");


    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return methodParameter.hasParameterAnnotation(RequestBody.class);
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        return new HttpInputMessage() {
            @Override
            public InputStream getBody() throws IOException {
                String bodyStr = IOUtils.toString(inputMessage.getBody(),"utf-8");
                if (StringUtils.isNotEmpty(bodyStr)) {
                    bodyStr = StringUtils.trim(Jsoup.clean(bodyStr, DEFAULT_WHITE_LIST));
                }
                return IOUtils.toInputStream(bodyStr,"utf-8");
            }

            @Override
            public HttpHeaders getHeaders() {
                return inputMessage.getHeaders();
            }
        };
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }
}

这边我重写了beforeBodyRead。从IO中读取字符串,在进行XSS数据清洗之后,并且再输出成IO流。虽然,在read部分又会进行一次IO流的读取。

小结

通过上面的一系列源码探索(这波探索真TM多),主要是针对JSON数据的XSS过滤方案的探索与构思。最后这个需求我做了如下的方案:

  1. 提供XssFilter进行XSS的数据过滤,这个是为了应对form表单提交的请求方式
  2. 提供XssRequestControllerAdvice和FastJsonXssValueFilter分别对请求和相应中的数据进行XSS过滤,这个是为了应对JSON数据的请求方式,前者可以防止后来的XSS数据写入,后者可以过滤已经存在系统当中的XSS脏数据,并且用户进行保存操作就可以纠正XSS数据。

最后