一、前言

跨站脚本攻击(Cross Site Scripting),为了不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS。恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。
使用Jsoup可以有效的过滤不安全的代码。Jsoup使用白名单的机制来预防XSS攻击,比如白名单中规定只允许<span> 标签的存在,那么其他标签都会被过滤掉。

二、常见的XSS攻击

比如页面的某个表单允许用户输入任意内容,当某个调皮的用户输入如下内容:

保存后,你会发现页面文字都变成了红色!

或者输入<script>for(var i=0;i<10;i++){alert("fuck you");},保存后页面将弹窗10次!

三、引入Jsoup

使用Maven构建一个简单的Spring Boot项目,在pom中引入:

1
2
3
4
5
6
       <!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.13.1</version>
</dependency>

四、JsoupUtil

创建一个JsoupUtil工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Xss过滤工具
*
*/
public class JsoupUtil {
private static final Whitelist whitelist = Whitelist.basicWithImages();
/*
* 配置过滤化参数,不对代码进行格式化
*/
private static final Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false);
static {
/*
* 富文本编辑时一些样式是使用style来进行实现的 比如红色字体 style="color:red;" 所以需要给所有标签添加style属性
*/
whitelist.addAttributes(":all", "style");
}

public static String clean(String content) {
return Jsoup.clean(content, "", whitelist, outputSettings);
}
}

这里采用的白名单为basicWithImages,Jsoup内置了几种常见的白名单供我们选择,如下表所示:

白名单对象标签说明
none只保留标签内文本内容
simpleTextb,em,i,strong,u简单的文本标签
basica,b,blockquote,br,cite,code,dd,dl,dt,em,i,li,ol,p,pre,q,small,span,strike,strong,sub,sup,u,ul基本使用的标签
basicWithImagesbasic 的基础上添加了 img 标签 及 img 标签的 src,align,alt,height,width,title 属性基本使用的加上 img 标签
relaxeda,b,blockquote,br,caption,cite,code,col,colgroup,dd,div,dl,dt,em,h1,h2,h3,h4,h5,h6,i,img,li,ol,p,pre,q,small,span,strike,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,u,ul在 basicWithImages 的基础上又增加了一部分部分标签

五、XssHttpServletRequestWrapper

创建一个XssHttpServletRequestWrapper,同过重写getParameter(),getParameterValues()和getHeader()方法来过滤HTTP请求中参数包含的恶意字符:

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
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {

HttpServletRequest orgRequest = null;

private boolean isIncludeRichText = false;

/**
* Constructs a request object wrapping the given request.
*
* @param request The request to wrap
* @throws IllegalArgumentException if the request is null
*/
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
orgRequest = request;
this.isIncludeRichText = isIncludeRichText;
}

/**
* 覆盖getParameter方法,将参数名和参数值都做xss过滤如果需要获得原始的值,则通过super.getParameterValues(name)来获取
* getParameterNames,getParameterValues和getParameterMap也可能需要覆盖
*/
@Override
public String getParameter(String name) {
if (("content".equals(name) || name.endsWith("WithHtml")) && !isIncludeRichText) {
return super.getParameter(name);
}
name = JsoupUtil.clean(name);
String value = super.getParameter(name);
if (StringUtils.isNotBlank(value)) {
value = JsoupUtil.clean(value);
}
return value;
}

@Override
public String[] getParameterValues(String name) {
String[] arr = super.getParameterValues(name);
if (arr != null) {
for (int i = 0; i < arr.length; i++) {
arr[i] = JsoupUtil.clean(arr[i]);
}
}
return arr;
}

/**
* 覆盖getHeader方法,将参数名和参数值都做xss过滤如果需要获得原始的值,则通过super.getHeaders(name)来获取
* getHeaderNames 也可能需要覆盖
*/
@Override
public String getHeader(String name) {
name = JsoupUtil.clean(name);
String value = super.getHeader(name);
if (StringUtils.isNotBlank(value)) {
value = JsoupUtil.clean(value);
}
return value;
}

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

/**
* 获取原始的request的静态方法
*/
public static HttpServletRequest getOrgRequest(HttpServletRequest req) {
if (req instanceof XssHttpServletRequestWrapper) {
return ((XssHttpServletRequestWrapper) req).getOrgRequest();
}
return req;
}
}

六、XssFilter

创建XssFilter,同过使用上面定义的XssHttpServletRequestWrapper类中的getParameter()等方法来保证参数得到了过滤:

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
@Slf4j
public class XssFilter implements Filter {

// 是否过滤富文本内容
private static boolean IS_INCLUDE_RICH_TEXT = false;

public List<String> excludes = new ArrayList<String>();

@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("------------ xss filter init ------------");
String isIncludeRichText = filterConfig.getInitParameter("isIncludeRichText");
if (StringUtils.isNotBlank(isIncludeRichText)) {
IS_INCLUDE_RICH_TEXT = BooleanUtils.toBoolean(isIncludeRichText);
}
String temp = filterConfig.getInitParameter("excludes");
if (temp != null) {
String[] url = temp.split(",");
for (int i = 0; url != null && i < url.length; i++) {
excludes.add(url[i]);
}
}
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("------------ xss filter doFilter ------------");
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if (handleExcludeURL(req, resp)) {
chain.doFilter(request, response);
return;
}
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request);
chain.doFilter(xssRequest, response);
}

@Override
public void destroy() {
log.info("------------ xss filter destroy ------------");

}

private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) {
if (excludes == null || excludes.isEmpty()) {
return false;
}
String url = request.getServletPath();
for (String pattern : excludes) {
Pattern p = Pattern.compile("^" + pattern);
Matcher m = p.matcher(url);
if (m.find())
return true;
}
return false;
}
}

七、Spring Boot中配置XssFilter

使用JavaConfig的形式配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean xssFilterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(new XssFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.setEnabled(true);
filterRegistrationBean.addUrlPatterns("/*");
Map<String, String> initParameters = new HashMap<String, String>();
initParameters.put("excludes", "/favicon.ico,/img/*,/js/*,/css/*");
initParameters.put("isIncludeRichText", "true");
filterRegistrationBean.setInitParameters(initParameters);
return filterRegistrationBean;
}
}