背景
最近所从事的项目,线上被扫描出部分连接存在XSS注入问题。例如:
http://www.xxx.com/yyy.html?applyId=5a20c06fa15243c109b66bb8%22%3E%3Csvg/οnlοad=alert(1)%3E&cate=&channel=0&isEmbed=0&level=0
上面连接中的 alert(1)脚本被执行。存在XSS漏洞。接下来开始解决,经过一个曲折的过程终于找到一个最佳方法。
可能的方案
我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:
- 在每个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的内容进行过滤(比较麻烦)。
笔者没有采用这种方式,继续寻找更优方案。。。
- 在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;
}
@Override
public 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;
}
@Override
public 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(){
@Override
public 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(){
@Override
public 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 {
@Override
protected 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注解的Bean
return 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类方法的参数值均会被过滤。
原文链接:https://blog.csdn.net/zhuangnet/article/details/78744004