彻底解决Spring MVC XSS注入问题

来源:互联网 发布:smt自动点胶机编程 编辑:程序博客网 时间:2024/06/05 19:21

一、背景

 最近所从事的项目,线上被扫描出部分连接存在XSS注入问题。例如:

http://www.xxx.com/yyy.html?applyId=5a20c06fa15243c109b66bb8%22%3E%3Csvg/onload=alert(1)%3E&cate=&channel=0&isEmbed=0&level=0


 上面连接中的 alert(1)脚本被执行。存在XSS漏洞。接下来开始解决,经过一个曲折的过程终于找到一个最佳方法。


 二、可能的方案

  

  1) 在每个Controller入口的业务代码处手动进行过滤,如:

   

    @RequestMapping("pcDetail.html")    public    @XssCheck    ModelAndView pcDetail(Integer tempId, String applyId) {        applyId = HtmlUtils.htmlEscape(applyId);

       这种方法最直接,但也最低级、最繁琐,每个入口都必须加一个过滤。 


   2) 百度上搜索 Spring mvc XSS关键词,出现最多的方案是 给Servlet加Filter,大致思路是:

       包装request->创建过滤器->添加过滤器

         通过扩展HttpServletRequestWrapper,对HttpServletRequest进行二次包装,覆盖其 public String[] getParameterValues(String name) 方法,在此方法中对各个参数值进行XSS过滤(Spring MVC 部分解析是调用的此方法)

但这种方法有个缺点:只能过滤GET请求,对于POST请求无能为力。对于POST请求,则还需要对 request.getInputStream的内容进行过滤(比较麻烦)。


  笔者没有采用这种方式,继续寻找更优方案。。。


  3)在Spring MVC流程中解决,能过自定义实现HandlerMethodArgumentResolver接口来自定义解析请求参数,在解析时做XSS过滤。

这种方法的话,解析过程比较繁琐、复杂,要考虑各种各样的客户端请求格式如json,form,xml等等。而且Spring MVC本身已经有非常完备的各种解析实现了。

为了一个XSS过滤又重新写一套,得不尝失。


笔者翻遍了Spring MVC的代码,也尝试过各种扩展,都不太理想。。。最后终于想到一个改动非常小,且可行办法。


三、可行的方案


 主要思路是:在Spring MVC调用Controller前,通过动态代理和反射机制对Controller的调用进行拦截,并在挡截中对Mehtod参数的值进行XSS过滤替换。

到这里,可能有人想说,Spring MVC本身就支持Controller拦截,即实现HandlerInterceptorAdapter接口。这种方法不可行,此接口无法实现对请求参数的修改。

话不多说,上代码(笔者基于Spring 3.2.4版本)。


1) 、  HandlerExecutionChainWrapper.java


public class HandlerExecutionChainWrapper extends HandlerExecutionChain {private BeanFactory beanFactory;private HttpServletRequest request;private HandlerMethod handlerWrapper;private byte[] lock = new byte[0];public HandlerExecutionChainWrapper(HandlerExecutionChain chain,HttpServletRequest request,BeanFactory beanFactory) {super(chain.getHandler(),chain.getInterceptors());this.request = request;this.beanFactory = beanFactory;}@Overridepublic Object getHandler() {if (handlerWrapper != null) {return handlerWrapper;}synchronized (lock) {if (handlerWrapper != null) {return handlerWrapper;}HandlerMethod superMethodHandler = (HandlerMethod)super.getHandler();Object proxyBean = createProxyBean(superMethodHandler);handlerWrapper = new HandlerMethod(proxyBean,superMethodHandler.getMethod());return handlerWrapper;}}/** * 为Controller Bean创建一个代理实例,以便用于 实现调用真实Controller Bean前的切面拦截 * 用以过滤方法参数中可能的XSS注入 * @param handler * @return */private Object createProxyBean(HandlerMethod handler) {try {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(handler.getBeanType());Object bean = handler.getBean();if (bean instanceof String) {bean = beanFactory.getBean((String)bean);}ControllerXssInterceptor xss = new ControllerXssInterceptor(bean);xss.setRequest(this.request);enhancer.setCallback(xss);return enhancer.create();}catch(Exception e) {throw new IllegalStateException("为Controller创建代理失败:"+e.getMessage(), e);}}public static class ControllerXssInterceptor implements MethodInterceptor {private Object target;private HttpServletRequest request;private List<String> objectMatchPackages;public ControllerXssInterceptor(Object target) {this.target = target;this.objectMatchPackages = new ArrayList<String>();this.objectMatchPackages.add("com.xx");}public void setRequest(HttpServletRequest request) {this.request = request;}@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {//对Controller的方法参数进行调用前处理//过滤String类型参数中可能存在的XSS注入if (args != null) {for (int i=0;i<args.length;i++) {if (args[i]==null)continue;if (args[i] instanceof String) {args[i] = stringXssReplace((String)args[i]);continue;}for(String pk:objectMatchPackages) {if (args[i].getClass().getName().startsWith(pk)) {objectXssReplace(args[i]);break;}}}}return method.invoke(target, args);}private String stringXssReplace(String argument) {return HtmlUtils.htmlEscape(argument);}private void objectXssReplace(final Object argument) {if (argument == null)return;ReflectionUtils.doWithFields(argument.getClass(), new FieldCallback(){@Overridepublic void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {ReflectionUtils.makeAccessible(field);String fv = (String)field.get(argument);if (fv != null) {String nv = HtmlUtils.htmlEscape(fv);field.set(argument, nv);}}}, new FieldFilter(){@Overridepublic boolean matches(Field field) {boolean typeMatch = String.class.equals(field.getType());if (request!=null && "GET".equals(request.getMethod())) {boolean requMatch = request.getParameterMap().containsKey(field.getName());return typeMatch && requMatch;}return typeMatch;}});}}}


2、DispatcherServletWrapper.java

@SuppressWarnings("serial")public class DispatcherServletWrapper extends DispatcherServlet {@Overrideprotected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {HandlerExecutionChain chain = super.getHandler(request);Object handler = chain.getHandler();        if (!(handler instanceof HandlerMethod)) {            return chain;        }        HandlerMethod hm = (HandlerMethod)handler;        if (!hm.getBeanType().isAnnotationPresent(Controller.class)) {        return chain;        }                //本扩展仅处理@Controller注解的Beanreturn new HandlerExecutionChainWrapper(chain,request,getWebApplicationContext());}}


3、替换Spring的DispatcherServlet为DispatcherServletWrapper 

<servlet><servlet-name>springmvc</servlet-name><!--  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  --><servlet-class>com.xx.sdd.mkt.web.spring.DispatcherServletWrapper</servlet-class><init-param>  <param-name>contextConfigLocation</param-name>  <param-value>classpath:spring-config-servlet.xml</param-value></init-param>        <load-on-startup>1</load-on-startup></servlet>


  大功告成,所有通过RequestMapping注解的Controller类方法的参数值均会被过滤。