什么是XSS攻击

XSS攻击全称为跨站脚本攻击。XSS是一种在web应用中的计算机安全漏洞,允许攻击者将恶意代码植入到网页中,导致其他正常用户访问该网页时被攻击。XSS最常见的做法就是在提交表单中插入html或者js脚本来达到攻击目的。这种攻击在互联网发展初期是很常见的,百度贴吧和新浪微博曾经都有被XSS攻击过。引起XSS攻击的关键原因还是在于开发时编写的程序不够严谨,没有对xss进行过滤。

虽然XSS攻击从出现到现在已经年代久远,但是他所产生的危害不容小视。当前主流的框架已经为我们提供好并且封装了各种功能,对XSS攻击也可以用简单的配置来过滤掉。不过还是那句话,框架只是工具,关键在于使用工具的人。如果遇到比较坑的开发人员,就算使用这些做了安全过滤的框架 任然可能因为使用不当造成被攻击的可能。

之前遇到的一家第三方软件公司,他们非常抵触用框架来开发,全部用jsp来写web程序,就和大学课堂上教的编程基础课一样,没有考虑到安全问题,自然有各种漏洞,没有写过滤器对XSS进行过滤,可以很轻易的就被攻击。

下面是简单的XSS攻击实例,使用jsp来搭建一个简单的网站,没有对xss进行过滤,主要就两个页面
1.查看信息列表页面

2.添加文章信息页面

正常的用户输入文字内容,点击保存跳转回信息列表页面,即可在信息列表页面查看到刚刚输入的文章内容。
如果是别有用心的攻击者,只需要输入html或者js即可完成注入。
这里我们输入一个js脚本用来弹出对话框,并插入一个 a标签注入url链接。

点击保存后跳转到信息列表页面时,我们可以看到原本正常的页面变得不正常了,浏览器跳出了对话框,对话框内容就是我们刚刚输入的js弹出内容,并且可以看到新增记录的文章内容变成了一个可以点击跳转的链接,如果攻击者输入了一个带有钓鱼网站的链接,那么其他用户在访问这个网页的时候就会看到和现在页面一样的情况,如果点击了链接就有可能中招。

试着添加一条文章,内容里面写html标签,看看能否用iframe来插入一个网页

点击保存,回到信息列表页面,发现文章内容被插入了一个网页

打开数据库,发现我们数据库直接存了用户提交的内容,没有进行任何转义处理。其他用户在查看信息列表页面时将查询出数据库的数据直接传给浏览器,浏览器会直接解释执行这些标签,给访问的用户造成危害。

我们应该添加一个Filter对用户输入的数据进行过滤
新建一个SecurityFilter类继承自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
package com.lanshiqin.student.filter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
* 安全过滤器
* 可以在doFilter中添加多个过滤规则
*/
public class SecurityFilter implements Filter {


@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

/**
* 过滤函数,可以添加多个规则
*
* @param servletRequest 请求对象
* @param servletResponse 响应对象
* @param filterChain 过滤器链
* @throws IOException 异常信息
* @throws ServletException servlet异常信息
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 过滤请求中的xss攻击
filterChain.doFilter(new XSSRequestWrapper((HttpServletRequest) servletRequest), servletResponse);
// 过滤或拦截其他攻击
// ...

}

@Override
public void destroy() {

}
}

新建一个XSSRequestWrapper类继承自HttpServletRequestWrapper类

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
142
143
144
145
146
147
148
149
150
package com.lanshiqin.student.filter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.regex.Pattern;

/**
* xss过滤规则包装类
*/
public class XSSRequestWrapper extends HttpServletRequestWrapper {

private HttpServletRequest orgRequest; // httpServlet请求对象

public XSSRequestWrapper(HttpServletRequest request) {
super(request);
orgRequest = request;
}

/**
* 覆盖getParameter方法,将参数名和参数值都做xss & sql过滤。
* 如果需要获得原始的值,则通过super.getParameterValues(name)来获取
* getParameterNames,getParameterValues和getParameterMap也可能需要覆盖
*/
@Override
public String getParameter(String name) {
String value = super.getParameter(xssEncode(name));
if (value != null) {
value = xssEncode(value);
}
return value;
}

/**
* 覆盖getHeader方法,将参数名和参数值都做xss & sql过滤。
* 如果需要获得原始的值,则通过super.getHeaders(name)来获取
* getHeaderNames 也可能需要覆盖
*/
@Override
public String getHeader(String name) {

String value = super.getHeader(xssEncode(name));
if (value != null) {
value = xssEncode(value);
}
return value;
}

/**
* 将容易引起xss & sql漏洞的半角字符直接替换成全角字符
*
* @param s 请求数据
* @return 编码后的请求数据
*/
private static String xssEncode(String s) {
if (s == null || s.isEmpty()) {
return s;
} else {
s = stripXSSAndSql(s);
}
StringBuilder sb = new StringBuilder(s.length() + 16);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '>':
sb.append(">");// 转义大于号
break;
case '<':
sb.append("<");// 转义小于号
break;
case '\'':
sb.append("'");// 转义单引号
break;
case '\"':
sb.append(""");// 转义双引号
break;
case '&':
sb.append("&");// 转义&
break;
case '#':
sb.append("#");// 转义#
break;
default:
sb.append(c);
break;
}
}
return sb.toString();
}

/**
* 获取最原始的request
*
* @return request
*/
public HttpServletRequest getOrgRequest() {
return orgRequest;
}

/**
* 获取最原始的request的静态方法
* 在装饰者设计模式下,除了过滤xss,可能还有多个过滤器包装类
* 提供静态方法方便继承该类的装饰者包装新的安全规则
*
* @return request请求对象
*/
public static HttpServletRequest getOrgRequest(HttpServletRequest req) {
if (req instanceof XSSRequestWrapper) {
return ((XSSRequestWrapper) req).getOrgRequest();
}
return req;
}

/**
* 防止xss跨脚本攻击(替换,根据实际情况调整)
* 通过正则匹配敏感字符
*/
public static String stripXSSAndSql(String value) {
if (value != null) {
// Avoid anything between script tags
Pattern scriptPattern = Pattern.compile("<[\r\n| | ]*script[\r\n| | ]*>(.*?)</[\r\n| | ]*script[\r\n| | ]*>", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid anything in a src="http://www.yihaomen.com/article/java/..." type of e-xpression
scriptPattern = Pattern.compile("src[\r\n| | ]*=[\r\n| | ]*[\\\"|\\\'](.*?)[\\\"|\\\']", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// Remove any lonesome </script> tag
scriptPattern = Pattern.compile("</[\r\n| | ]*script[\r\n| | ]*>", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// Remove any lonesome <script ...> tag
scriptPattern = Pattern.compile("<[\r\n| | ]*script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid eval(...) expressions
scriptPattern = Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid e-xpression(...) expressions
scriptPattern = Pattern.compile("e-xpression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid javascript:... expressions
scriptPattern = Pattern.compile("javascript[\r\n| | ]*:[\r\n| | ]*", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid vbscript:... expressions
scriptPattern = Pattern.compile("vbscript[\r\n| | ]*:[\r\n| | ]*", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid onload= expressions
scriptPattern = Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
}
return value;
}

}

在项目的web.xml中添加安全过滤器配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">

<!--安全过滤器配置-->
<filter>
<filter-name>securityFilter</filter-name>
<filter-class>com.lanshiqin.student.filter.SecurityFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>securityFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>

</web-app>

可以看到我们的过滤器作用域为REQUES,匹配路径我们配置成/*,这样访问该网站的所有路径的请求都会走到我们的过滤器进行过滤。我们过滤器主要是做了字符匹配替换的操作。把XSS的半角字符替换成全角字符,主要其实过滤掉尖括号标签,让非法用户注入的html标签和js被破坏,页面提交重放后无法正常执行注入的html和js。

加上过滤器后我们重新运行网站,再次进行XSS注入测试。


提交保存后,回到信息列表页面,底下两条是我们刚刚最新添加的文章,输入内容直接被显示出来了,url连接也无法注入了。

原本的半角字符被替换成全角,所以xss被破坏就无法被执行了。上面的转换规则可以过滤掉一些简单的XSS注入,如果你的网站后台需要接受一个json格式的数据,提交的json数据里半角双引号会被转换成全角双引号,这时候就需要更加业务需求修改规则,比如对符号进行转义,而不是替换成全角。我们主要过滤的其实是尖括号标签,去掉头尾,一般的xss就会被破坏。

很多论坛网站的文章内容输入表单,一般都是用类似于UEditer的富文本框来接收用户的输入,会把用户输入的内容转义成html标签转义字符给后台,后台在存储到数据库中,由于数据库存放的已经是转义后的标签,这样就不用担心用户输入了html或者js等内容。保存的文章被查询出来时是被转义的字符,浏览器无法执行这个被转义的标签,而是当做字符串被转义成内容显示在网页上。这样就过滤了XSS攻击

0%