点击上方蓝字关注我们
奇怪的XSS过滤器
一般在实际Web开发中,我们常常需要过滤/编码页面传递给后台的特殊字符,防止xss跨站脚本攻击。使用过滤器Filter可以很好的完成这个功能。前段时间某项目的研发使用过滤器进行XSS过滤后,进行复测,发现一个奇怪的现象,原本网站全局的xss,变成了只有部分接口存在xss。
一开始以为过滤器的匹配接口路径存在问题,查看代码发现开发是通过写HttpServletRequestWrapper的Method对所有的request进行处理的:
相关代码如下,系统使用SSM开发:
首先定义一个filter,通过重写HttpServletRequestWrapper的方法修改request参数:
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { SecHttpRequestWrapper reqwrapper = new SecHttpRequestWrapper((HttpServletRequest) request); System.out.println("filter in"); chain.doFilter(reqwrapper, response); return; }
继承HttpServletRequestWrapper,重写getParameter方法,将参数名和参数值都做xss过滤:
public class SecHttpRequestWrapper extends HttpServletRequestWrapper { private HttpServletRequest request; public SecHttpRequestWrapper(HttpServletRequest request){ super(request); this.request = request; } /** * 覆盖getParameter方法,将参数名和参数值都做xss过滤。 */ @Override public String getParameter(String name) { if (value != null) { value = xssEncode(value); } return value; } public static String xssEncode(String value) { //xss处理 }}
通过上面的操作,前端传递过来的内容就可以在Controller中使用request.getParameter(“xxx”)的形式获取到之后,然后进行xss编码处理,从而在进行view层展示或者写入数据库前保证系统免受xss跨站脚本攻击的困扰了。
例如如下测试接口:
@RequestMapping(value = "/servlet_req_param",method = { RequestMethod.POST, RequestMethod.GET }) public String req_param(HttpServletRequest request){ return request.getParameter("content"); }
将content参数的值设置为
可以看到输入的恶意xsspayload的确是经过相关的编码操作的。
这么梳理下来好像的确是这么回事,但是结合测试,的确是过滤器失效了部分接口仍可进行xss利用。看一下仍可进行xss利用的接口,发现一个问题就是都使用了@RequestParam注解和POJO对象绑定的方式进行传参。
复盘
刚刚提到,问题接口都使用了@RequestParam注解和POJO对象绑定的方式进行传参。
在实际开发中,软件框架可以自动将HTTP请求参数绑定到程序代码变量或者对象中,从而使得更易开发。在SpringMVC中,提交请求的数据是通过方法形参来接收的。从客户端请求的key/value数据,经过参数绑定,将key/value数据绑定到Controller的形参上,然后在Controller就可以直接使用该形参。
那么就来看看@RequestParam注解和POJO对象绑定的方式跟过滤器之间有什么联系好了。使用@RequestParam注解进行参数绑定,测试接口如下:
@RequestMapping(value = "/req_param2",method = { RequestMethod.POST, RequestMethod.GET }) public String req_param(ModelMap modelmap, @RequestParam("content") String content) { return content; }
将content参数的值设置为
这里输入的content经过filter后原样输出了,并没有经过xssEncode()方法进行过滤/编码操作。
再试试POJO对象绑定的方式,测试接口如下:
@RequestMapping(value = "/user_model_req",method = { RequestMethod.POST, RequestMethod.GET }) public User user_model_req(ModelMap modelmap, User user) { if (user == null) { return null; } return user; }
User类:
public class User { public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } private String username; public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } private String password;}
跟前面的一样,将User属性的值设置为
这里输入的User属性经过filter后同样也原样输出了,并没有经过xssEncode()方法进行过滤/编码操作。
再试试基本String类型的参数绑定,测试接口如下:
@RequestMapping(value = "/req_param1",method = { RequestMethod.POST, RequestMethod.GET }) public String req_var(ModelMap modelmap, String content) { return content; }
同样的,将content参数的值设置为
以上的结果都跟我们的预期相悖,出现了Filter无法覆盖的场景。
再回头看一下我们的filter,我们是通过重写HttpServletRequestWrapper的getParameter()方法进行参数编码/过滤的,也就是说:
springmvc直接绑定pojo或者@RequestParam(基本数据类型绑定)没法在HttpServletRequestWrapper的getparameter方法进行过滤操作。
到底漏了什么
猜测很大可能是跟HttpServletRequestWrapper有关系。查看相关的doc.https://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServletRequestWrapper.html,可以看到,除了getparameter方法以外,HttpServletRequestWrapper因为继承自javax.servlet.http.HttpServletRequest,还存在多种方法:
getParameterValues
getParameterNames
getParameterMap
…
那么上面无法覆盖的问题很可能是直接绑定pojo或者@RequestParam(基本数据类型绑定)获取参数值的方法不一致,从而导致再重写HttpServletRequestWrapper的Method后无法满足我们的安全需要。
查看注解@RequestParam的具体实现方式,在org.springframework.web.bind.annotation.support.HandlerMethodInvoker的resolveRequestParam中看到如下代码:
if (paramValue == null) { String[] paramValues = webRequest.getParameterValues(paramName); if (paramValues != null) { paramValue = (paramValues.length == 1 ? paramValues[0] : paramValues); } } if (paramValue == null) { if (defaultValue != null) { paramValue = resolveDefaultValue(defaultValue); } else if (required) { raiseMissingParameterException(paramName, paramType); } paramValue = checkValue(paramName, paramValue, paramType);}
那么也就是说注解@RequestParam是使用getParameterValues方法来获取参数值的,而不是getParameter方法。所以只需要重写HttpServletRequestWrapper的getParameterValues方法应该就可以解决上述无法覆盖的问题了。
下面实验看一看,重写getParameterValues方法后打印下log:
public String[] getParameterValues(String parameter) { System.out.println("filter in getParameterValues Method"); String[] values = super.getParameterValues(parameter); if (values==null) { return null; } int count = values.length; String[] encodedValues = new String[count]; for (int i = 0; i > count; i++) { encodedValues[i] = xssEncode(values[i]); } return encodedValues; }
跟前面的一样,将content参数的值设置为
可以看到成功在getParameterValues拦截了我们的输入并且在xssEncode()方法进行了过滤/编码操作。印证了前面的想法。
同理,POJO绑定猜测也是通过getParameterValues进行参数值的获取,这里将User属性的值设置为
可以看到成功在getParameterValues拦截了我们的输入并且在xssEncode()方法进行了过滤/编码操作,也就是说POJO绑定猜测也是通过getParameterValues进行参数值的获取的。
也就是说,springmvc直接绑定pojo或者@RequestParam(基本数据类型绑定)可以在HttpServletRequestWrapper的getparameterValues方法进行过滤操作。另外搭建了一下环境测试了一下Springboot,也是同样的效果。
总结与延伸filter的实际开发设计时需要关注的点很多,一不注意很可能就会导致我们的安全措施被绕过。在通过重写HttpServletRequestWrapper的Method进行filter设计的时候,要额外关注上述的细节。根据上述场景,进一步引申,可能getParameterNames,getParameterValues和getParameterMap等方法也可能需要覆盖。同理,排除xss情况,例如我们通过header中的token进行权限校验的时候,getHeaderNames方法也可能需要覆盖,否则可能在某些特定场景下相关的参数值无法进入filter管理,导致权限绕过缺陷。
第一时间给你最新技术干货在这里,探索技术与热爱! 点击此处“阅读全文”查看更多内容