XSS介绍
XSS(Cross-Site Scripting)是一种常见的 Web 攻击方式,攻击者通过注入恶意脚本代码到受信任的网站中,使用户在浏览器上执行该恶意代码,从而实现攻击目的。对于 XSS 的介绍资料非常多,可以自行查找,这里仅简单科普一下。
XSS 攻击可以分为三种类型:
- 存储型 XSS:攻击者将恶意脚本存储到目标网站的数据库中,当用户访问包含恶意代码的页面时,恶意代码会从服务器传送到用户的浏览器执行。
- 反射型 XSS:攻击者构造一个包含恶意脚本的 URL,诱使用户点击该 URL,服务器接收到请求后,将恶意脚本作为响应的一部分返回给用户的浏览器执行。
- DOM 型 XSS:攻击者通过修改页面的 DOM 结构,篡改页面的行为,使恶意代码在用户浏览器中执行。
为了防范 XSS 攻击,一般可以有下面措施:
- 输入验证和过滤:对用户输入的数据进行验证,确保输入符合预期格式,过滤掉特殊字符和敏感代码。
- 输出编码:在将用户输入的数据输出到页面时,进行合适的编码,如 HTML 转义,以确保恶意代码不会被执行。
- 使用 HTTP-only Cookie:将敏感信息存储在 HTTP-only Cookie 中,避免通过 JavaScript 访问敏感信息。
- Content Security Policy (CSP):使用 CSP 设置白名单,限制页面加载的资源来源,防止恶意脚本的注入。
- 防止 DOM 操作:避免使用 innerHTML、eval 等可以执行脚本的方法,优先使用更安全的 DOM 操作方法。
- 使用安全的框架和库:选择安全性较高的前端框架和库,它们通常会内置一些防范 XSS 攻击的机制。
- 定期更新和修复:及时关注安全漏洞和最佳实践,持续更新和修复应用程序中的安全问题。
服务端检测与过滤
对于存储型XSS和反射型XSS,要实现攻击,都必须经过服务端,因此,可以对请求参数中的XSS标签进行检测,实现XSS攻击拦截。
最简单的处理方式,就是不允许用户在提交内容中使用HTML来避免XSS攻,强制用户使用纯文本,或者使用其他标记语法,如Markdown。这种处理方式会降低表达性,并且迫使用户学些新的语法。
另一种处理方式是允许用户在提交内容中使用HTML,但是不允许使用不安全的标记和属性。实现上,用户可以通过正则匹配的方式,检查用户提交的内容中是否包含不安全的标记及属性,并进行过滤。如下面正则:
swift
Pattern[] XSS_PATTERNS = new Pattern[] {
Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE),
Pattern.compile("<script>", Pattern.CASE_INSENSITIVE),
Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),
Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("<img>(.*?)</img>", Pattern.CASE_INSENSITIVE),
Pattern.compile("<img>", Pattern.CASE_INSENSITIVE),
Pattern.compile("</img>", Pattern.CASE_INSENSITIVE),
Pattern.compile("<img(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("<(.*?)img/>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("eval\((.*?)\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("expression\((.*?)\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE),
Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE),
Pattern.compile("(['"])\s*on(click|context|mouse|dblclick|key|abort|error|before|hash|load|page|"
+ "resize|scroll|unload|blur|change|focus|in|reset|search|select|submit|drag|drop|copy|cut|paste|"
+ "after|before|can|end|duration|emp|play|progress|seek|stall|sus|time|volume|waiting|message|open|touch|"
+ "on|off|pop|show|storage|toggle|wheel)(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
};
正则可以实现提交内容的检测与拦截,但是要写出所有的标记与属性检测正则,难度极大,而且特别容易误拦、漏拦。此外,正则检测也会非常耗时,影响服务性能。因此,为了实现精确的XSS检测,必须要实现标签识别,这个就可以从HTML语法规则入手,通过遍历HTML抽象语法树来识别XSS标签。
本文要介绍的XSS标签检测与过滤方式就是通过解析并遍历HTML解析树的方式实现,从HTML语法规则入手,检测输入内容中是否只包含已知安全的标签,对于不安全的标签可以进行过滤,而后生成安全的文本内容提交到服务端。
HTML语法树解析
HTML语言与其他语言一样,也是由语法规则及词汇符号,对HTML的解析实就是将输入的一系列字符拆分成词法符号Token,并按语法规则生成一颗语法树的过程。得到语法树后,可以通过分析语法树实现语句含义的识别。

如上图一条赋值语句sp = 100;
,将输入字符流拆分成一个个不可再分的词法符号的过程为词法分析,将词法符号流转换成语法分析树的过程为语法分析。
词法分析器和语法分析器有很多种实现,这里不做详细介绍,感兴趣的可以参考《从语言识别到通用SQL解析》一文。这里我们以Antlr4为例,可以实现一个基于Antlr4的HTML词法和语法分析器。
xml
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
Hello World!
</body>
</html>
对于上面一段HTML文本,经过词法和语法分析器,可以得到下面一棵HTML解析树,HTML中每个标签都会对应HTML解析树中的一个节点,通过遍历这棵HTML解析树,就可以知道该HTML文本有哪些标签,是否包含了不安全的标签。

JSOUP实现HTML解析
上面介绍了可以通过Antlr4实现HTML语法树的解析,但Antlr4是一种通用的语言识别器,必须要自己去遍历语法树,较为复杂。在Java领域,jsoup是一个专门用于HTML5语言识别器库,可以将HTML文本解析成类似浏览器解析的DOM。此外,jsoup提供了丰富的功能用于XSS标签检测与过滤。
Maven包引入:
xml
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.16.2</version>
</dependency>
HTML文本解析:
xml
String html = "<!DOCTYPE html>\n" +
"<html>\n" +
" <head>\n" +
" <meta charset="utf-8">\n" +
" <title>我是标题</title>\n" +
" </head>\n" +
" <body>\n" +
" Hello World!\n" +
" </body>\n" +
"</html>";
Document document = Jsoup.parse(html);

通过Document
类提供的方法,比如head()、body()、title()等,我们会非常方便的获取HTML中的内容,通过增删节点,也会很方便地改变HTML语法树,这些都不是本文关注点,这里不再过多介绍,感兴趣的自己去研究。
XSS标签检测与拦截
使用jsoup实现XSS检测,我们首先看一个例子。比如下面一段HTML文本,包含了onlick标签,如果作为请求参数带给服务端,会产生XSS攻击。
xml
<p><a href='http://www.demo.com/' onclick='sendCookiesToMe()'>Link</a></p>
我们对这段HTML文本使用jsoup进行XSS标签过滤,会得到下面控制台的输出文本,此时,XSS标签已经被过滤掉。
ini
String dirtyHTML = "<p><a href='http://www.demo.com/' onclick='sendCookiesToMe()'>Link</a></p>";
String cleanHTML = Jsoup.clean(dirtyHTML, Safelist.basic());
System.out.println(cleanHTML);
控制台输出:<p><a href="http://www.demo.com/" rel="nofollow">Link</a></p>
XSS标签过滤
jsoup工具包提供了Jsoup#clean
实现XSS标签的过滤,在过滤时可以自定义安全标签,不在安全标签内的都会被过滤。jsoup默认提供了5种安全标签列表,分别为none
、simpleText
、basic
、basicWithImages
、relaxed
。下面先用一个例子来直观了解下这5种安全标签列表的区别。
xml
<p><a href='http://demo.com' onclick='sendCookies()'><b>Link</b></a><img src='http://demo.com/simple.gif'></p><div>Test</div>
对上面一段HTML文本执行去除XSS标签清理的操作。
ini
String dirtyHTML = "<p><a href='http://demo.com' onclick='sendCookies()'><b>Link</b></a><img src='http://demo.com/simple.gif'></p><div>Test</div>";
String noneHTML = Jsoup.clean(dirtyHTML, Safelist.none());
String simpleTextHTML = Jsoup.clean(dirtyHTML, Safelist.simpleText());
String basicHTML = Jsoup.clean(dirtyHTML, Safelist.basic());
String basicWithImagesHTML = Jsoup.clean(dirtyHTML, Safelist.basicWithImages());
String relaxedHTML = Jsoup.clean(dirtyHTML, Safelist.relaxed());
System.out.println("none: " + noneHTML);
System.out.println("simpleText: " + simpleTextHTML);
System.out.println("basic: " + basicHTML);
System.out.println("basicWithImages: " + basicWithImagesHTML);
System.out.println("relaxed: " + relaxedHTML);
控制台输出结果如下:
xml
none: LinkTest
simpleText: <b>Link</b>Test
basic: <p><a href="http://demo.com" rel="nofollow"><b>Link</b></a></p>Test
basicWithImages: <p><a href="http://demo.com" rel="nofollow"><b>Link</b></a><img src="http://demo.com/simple.gif"></p>Test
relaxed: <p><a href="http://demo.com"><b>Link</b></a><img src="http://demo.com/simple.gif"></p>
<div>
Test
</div>
可以看出,5种类型支持的安全标签越来越多,下面详细介绍下:
- none:这个安全列表只允许文本节点,任何HTML元素或除TextNode以外的任何节点都将被删除。
- simpleText:这个安全列表只允许简单的文本格式,包括:
b
、em
、i
、strong
、u
,所有其他HTML标签和属性将被删除。 - basic:这个安全列表允许更全面的文本节点,包括:
a
、b
、blockquote
、br
、cite
、code
、dd
、dl
、dt
、em
、i
、li
、ol
、p
、pre
、q
、small
、span
、strike
、strong
、sub
、sup
、u
、ul
和适当的属性。a
标签可以指向http
、https
、ftp
、mailto
,并且属性会被强制为rel=nofollow。该安全列表不支持图片。 - basicWithImages:该安全列表在basic的基础上增加对图片的支持,但图片必须是绝对地址,不可以是相对地址。
- relaxed:该安全列表允许使用各种文本和结构体HTML,包括
a
、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
。此外,链接没有强制的rel=nofollow属性。
JSOUP安全列表增强
jsoup工具包提供了多种默认安全列表,如果relaxed
,可以尽可能地保留更多安全标签,但也同样存在问题。relaxed
安全列表要求a
、img
等标签必须以http、 https等开头,必须为绝对路径,否则会被过滤掉。我们来看一个例子:
xml
String dirtyHTML = "<p><a href='http://demo.com' onclick='sendCookies()'><b>Link</b></a><img src='simple.gif'></p><div>Test</div>";
String relaxedHTML = Jsoup.clean(dirtyHTML, Safelist.relaxed());
System.out.println("relaxed: " + relaxedHTML);
控制台输出结果为:
javascript
relaxed: <p><a href="http://demo.com"><b>Link</b></a><img></p>
<div>
Test
</div>
可见,img
标签中,使用相对路径的src
被过滤掉,导致误判。我们可以对relaxed
安全列表进行增强,避免类似误判。如下:
java
public static class HtmlSafeList extends org.jsoup.safety.Safelist {
public static final HtmlSafeList INSTANCE = new HtmlSafeList();
public HtmlSafeList() {
addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "span", "embed", "object", "dl", "dt",
"em", "h1", "h2", "h3", "h4", "h5", "h6", "i", "img", "li", "ol", "p", "pre", "q", "small",
"strike", "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul");
addAttributes("a", "href", "title", "target");
addAttributes("blockquote", "cite");
addAttributes("col", "span");
addAttributes("colgroup", "span");
addAttributes("img", "align", "alt", "src", "title");
addAttributes("ol", "start");
addAttributes("q", "cite");
addAttributes("table", "summary");
addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width");
addAttributes("th", "abbr", "axis", "colspan", "rowspan", "scope", "width");
addAttributes("video", "src", "autoplay", "controls", "loop", "muted", "poster", "preload");
addAttributes("object", "width", "height", "classid", "codebase");
addAttributes("param", "name", "value");
addAttributes("embed", "src", "quality", "width", "height", "allowFullScreen", "allowScriptAccess", "flashvars", "name", "type", "pluginspage");
addAttributes(":all", "class", "style", "height", "width", "type", "id", "name");
addProtocols("blockquote", "cite", "http", "https");
addProtocols("cite", "cite", "http", "https");
addProtocols("q", "cite", "http", "https");
// 如果添加以下的协议,那么href必须是http、 https等开头,相对路径则被过滤掉了
// addProtocols("a", "href", "ftp", "http", "https", "mailto", "tel");
// 如果添加以下的协议,那么src必须是http或者https开头,相对路径则被过滤掉了,
// 所以必须注释掉,允许相对路径的图片资源
// addProtocols("img", "src", "http", "https");
}
@Override
protected boolean isSafeAttribute(String tagName, Element el, Attribute attr) {
// 不允许javascript开头的src和href
if ("src".equalsIgnoreCase(attr.getKey()) || "href".equalsIgnoreCase(attr.getKey())) {
String value = attr.getValue();
if (StringUtils.hasText(value) && value.toLowerCase().startsWith("javascript")) {
return false;
}
}
// 允许base64的图片内容
if ("img".equals(tagName) && "src".equals(attr.getKey()) && attr.getValue().startsWith("data:;base64")) {
return true;
}
return super.isSafeAttribute(tagName, el, attr);
}
}
再来测试一下过滤结果:
xml
String dirtyHTML = "<p><a href='http://demo.com' onclick='sendCookies()'><b>Link</b></a><img src='simple.gif'></p><div>Test</div>";
String relaxedHTML = Jsoup.clean(dirtyHTML, HtmlSafeList.INSTANCE);
System.out.println("relaxed: " + relaxedHTML);
控制台输出如下:
javascript
relaxed: <p><a href="http://demo.com"><b>Link</b></a><img src="simple.gif"></p>
<div>
Test
</div>
XSS标签检测
XSS标签过滤会将原来文本中的不安全标签删除或者替换掉,这样服务端存储的内容和用户输入的内容不一致,不一定能符合产品需求。在服务端实现时,可以通过检测输入文本中是否存在不安全的标签,如果存在则拒绝处理。我们仍然可以利用上一节的XSS标签过滤方案,将过滤后的文本与过滤前的文本进行比对,如果不一致,说明输入文本中存在不安全标签。理想很丰满,现实很骨感。上述XSS标签过滤即使不存在不安全标签,在语法树重新转换成字符串时,也可能会调整结构,生成的文本内容和原始内容不一致,导致检测误判。对此,jsoup工具包提供了Jsoup#isValid
实现不安全标签的检测,测试输入的HTML是否只有安全列表允许的标记和属性,用于表单验证。
还是以上面的HTML文本为例,执行XSS标签检测:
xml
String dirtyHTML = "<p><a href='http://demo.com' onclick='sendCookies()'><b>Link</b></a><img src='http://demo.com/simple.gif'></p><div>Test</div>";
boolean valid = Jsoup.isValid(dirtyHTML, Safelist.relaxed());
System.out.println("valid: " + valid);
控制台输出结果如下:
vbnet
valid: false
因此,可以利用Jsoup#isValid
实现不安全标签的检测,避免出现服务端存储内容和用户期望内容不一致的问题。但是。JSOUP提供的检测方法也有问题,我们还是以一个例子先来说明。
xml
String dirtyHTML1 = "<p><a href='http://demo.com'><b>Link</b></a><img src='simple.gif'></p><div>Test</div>";
String dirtyHTML2 = "<p><a href='http://demo.com'<b>Link</b></a><img src='simple.gif'></p><div>Test</div>";
boolean valid1 = Jsoup.isValid(dirtyHTML1, HtmlSafeList.INSTANCE);
boolean valid2 = Jsoup.isValid(dirtyHTML2, HtmlSafeList.INSTANCE);
System.out.println("valid1: " + valid1);
System.out.println("valid2: " + valid2);
上述两个HTML文本,第一个文本a
标签有结束符>
,第二个文本a
标签缺少结束符>
。控制台输出结果如下:
vbnet
valid1: true
valid2: false
两个输入文本中,均不包含XSS不安全标签,第一个文本检测结果为有效文本,而第二个文本检测结果却不是有效文本,不符合我们的检测预期。我们来看下Jsoup#isValid
方法的实现。在isValid
方法中调用了isValidBodyHtml
方法,从方法名就可以判断出该方法的作用是检测输入文本是否是有效的HTML,包含两层含义,一是符合HTML规则的文本,二是不包含不安全的XSS标签。从isValidBodyHtml
方法实现看,最后返回的内容为numDiscarded == 0 && errorList.isEmpty()
,numDiscarded == 0
是判断输入文本是否包含不安全的XSS标签,errorList.isEmpty()
则是判断该文本是否为有效的HTML文本,判断逻辑不符合预期。
java
/**
Test if the input body HTML has only tags and attributes allowed by the Safelist. Useful for form validation.
<p>
This method is intended to be used in a user interface as a validator for user input. Note that regardless of the
output of this method, the input document <b>must always</b> be normalized using a method such as
{@link #clean(String, String, Safelist)}, and the result of that method used to store or serialize the document
before later reuse such as presentation to end users. This ensures that enforced attributes are set correctly, and
that any differences between how a given browser and how jsoup parses the input HTML are normalized.
</p>
<p>Example:</p>
<pre>{@code
Safelist safelist = Safelist.relaxed();
boolean isValid = Jsoup.isValid(sourceBodyHtml, safelist);
String normalizedHtml = Jsoup.clean(sourceBodyHtml, "https://example.com/", safelist);
}</pre>
<p>Assumes the HTML is a body fragment (i.e. will be used in an existing HTML document body.)
@param bodyHtml HTML to test
@param safelist safelist to test against
@return true if no tags or attributes were removed; false otherwise
@see #clean(String, Safelist)
*/
public static boolean isValid(String bodyHtml, Safelist safelist) {
return new Cleaner(safelist).isValidBodyHtml(bodyHtml);
}
/**
Determines if the input document's <b>body HTML</b> is valid, against the safelist. It is considered valid if all
the tags and attributes in the input HTML are allowed by the safelist.
<p>
This method is intended to be used in a user interface as a validator for user input. Note that regardless of the
output of this method, the input document <b>must always</b> be normalized using a method such as
{@link #clean(Document)}, and the result of that method used to store or serialize the document before later reuse
such as presentation to end users. This ensures that enforced attributes are set correctly, and that any
differences between how a given browser and how jsoup parses the input HTML are normalized.
</p>
<p>Example:
<pre>{@code
Document inputDoc = Jsoup.parse(inputHtml);
Cleaner cleaner = new Cleaner(Safelist.relaxed());
boolean isValid = cleaner.isValidBodyHtml(inputHtml);
Document normalizedDoc = cleaner.clean(inputDoc);
}</pre>
</p>
@param bodyHtml HTML fragment to test
@return true if no tags or attributes need to be removed; false if they do
*/
public boolean isValidBodyHtml(String bodyHtml) {
Document clean = Document.createShell("");
Document dirty = Document.createShell("");
ParseErrorList errorList = ParseErrorList.tracking(1);
List<Node> nodes = Parser.parseFragment(bodyHtml, dirty.body(), "", errorList);
dirty.body().insertChildren(0, nodes);
int numDiscarded = copySafeNodes(dirty.body(), clean.body());
return numDiscarded == 0 && errorList.isEmpty();
}
我们对原生的JSOUP方法进行改造,因copySafeNodes
是私有方法,需要通过反射方式来访问。改造后如下:
ini
/**
* xss 检测
*
* @link Cleaner#isValidBodyHtml实现
*
* @param html html
* @param ignoreHtmlValidity 是否忽略html的有效性
* @return 是否含xss
*/
public static boolean has(String html, boolean ignoreHtmlValidity) {
if (!StringUtils.hasText(html)) {
return false;
}
Document clean = Document.createShell("");
Document dirty = Document.createShell("");
ParseErrorList errorList = ParseErrorList.tracking(1);
List<Node> nodes = Parser.parseFragment(html, dirty.body(), "", errorList);
dirty.body().insertChildren(0, nodes);
Cleaner cleaner = new Cleaner(WHITE_LIST);
try {
Method method = Cleaner.class.getDeclaredMethod("copySafeNodes", Element.class, Element.class);
method.setAccessible(true);
int numDiscarded = (int) method.invoke(cleaner, dirty.body(), clean.body());
boolean hasXss = numDiscarded != 0;
if (ignoreHtmlValidity) {
return hasXss;
}
boolean validHtml = errorList.isEmpty();
return validHtml && hasXss;
} catch (Exception e) {
throw new RuntimeException("检查XSS失败", e);
}
}
还是用上面的例子来测试:
ini
String dirtyHTML1 = "<p><a href='http://demo.com'><b>Link</b></a><img src='simple.gif'></p><div>Test</div>";
String dirtyHTML2 = "<p><a href='http://demo.com'<b>Link</b></a><img src='simple.gif'></p><div>Test</div>";
String dirtyHTML3 = "<p><a href='http://demo.com' onclick='sendCookies()'><b>Link</b></a><img src='simple.gif'></p><div>Test</div>";
String dirtyHTML4 = "<p><a href='http://demo.com' onclick='sendCookies()'<b>Link</b></a><img src='simple.gif'></p><div>Test</div>";
boolean valid11 = XssUtil.has(dirtyHTML1, true, XssUtil.HtmlSafeList.INSTANCE);
boolean valid12 = XssUtil.has(dirtyHTML1, false, XssUtil.HtmlSafeList.INSTANCE);
boolean valid21 = XssUtil.has(dirtyHTML2, true, XssUtil.HtmlSafeList.INSTANCE);
boolean valid22 = XssUtil.has(dirtyHTML2, false, XssUtil.HtmlSafeList.INSTANCE);
boolean valid31 = XssUtil.has(dirtyHTML3, true, XssUtil.HtmlSafeList.INSTANCE);
boolean valid32 = XssUtil.has(dirtyHTML3, false, XssUtil.HtmlSafeList.INSTANCE);
boolean valid41 = XssUtil.has(dirtyHTML4, true, XssUtil.HtmlSafeList.INSTANCE);
boolean valid42 = XssUtil.has(dirtyHTML4, false, XssUtil.HtmlSafeList.INSTANCE);
System.out.println("valid11: " + valid11);
System.out.println("valid12: " + valid12);
System.out.println("valid21: " + valid21);
System.out.println("valid22: " + valid22);
System.out.println("valid31: " + valid31);
System.out.println("valid32: " + valid32);
System.out.println("valid41: " + valid41);
System.out.println("valid52: " + valid42);
控制台输入结果如下,检测结果均符合预期。
vbnet
valid11: false
valid12: false
valid21: false
valid22: false
valid31: true
valid32: true
valid41: true
valid52: false
实现源码
- Maven引入
xml
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.16.2</version>
</dependency>
- XSS工具类
java
public class XssUtil {
private XssUtil() {
}
private static final HtmlSafeList WHITE_LIST = HtmlSafeList.INSTANCE;
private static final Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings()
// 保留换行符
.prettyPrint(false);
/**
* xss 清理(部分非XSS的内容也会被清理掉)
*
* @param html html
* @return 清理后的 html
*/
public static String clean(String html) {
if (StringUtils.hasText(html)) {
return Jsoup.clean(html, "", WHITE_LIST, OUTPUT_SETTINGS);
}
return html;
}
/**
* xss 检测
*
* @link Cleaner#isValidBodyHtml实现
*
* @param html html
* @param ignoreHtmlValidity 是否忽略html的有效性
* @param safelist 安全标签
* @return 是否含xss
*/
public static boolean has(String html, boolean ignoreHtmlValidity) {
return has(html, ignoreHtmlValidity, WHITE_LIST);
}
/**
* xss 检测
*
* @link Cleaner#isValidBodyHtml实现
*
* @param html html
* @param ignoreHtmlValidity 是否忽略html的有效性
* @param safelist 安全标签
* @return 是否含xss
*/
public static boolean has(String html, boolean ignoreHtmlValidity, Safelist safelist) {
if (!StringUtils.hasText(html)) {
return false;
}
Document clean = Document.createShell("");
Document dirty = Document.createShell("");
ParseErrorList errorList = ParseErrorList.tracking(1);
List<Node> nodes = Parser.parseFragment(html, dirty.body(), "", errorList);
dirty.body().insertChildren(0, nodes);
Cleaner cleaner = new Cleaner(safelist);
try {
Method method = Cleaner.class.getDeclaredMethod("copySafeNodes", Element.class, Element.class);
method.setAccessible(true);
int numDiscarded = (int) method.invoke(cleaner, dirty.body(), clean.body());
boolean hasXss = numDiscarded != 0;
if (ignoreHtmlValidity) {
return hasXss;
}
boolean validHtml = errorList.isEmpty();
return validHtml && hasXss;
} catch (Exception e) {
throw new RuntimeException("检查XSS失败", e);
}
}
/**
* 做自己的白名单,允许base64的图片通过等
*
* @author michael
*/
public static class HtmlSafeList extends org.jsoup.safety.Safelist {
public static final HtmlSafeList INSTANCE = new HtmlSafeList();
public HtmlSafeList() {
addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "span", "embed", "object", "dl", "dt",
"em", "h1", "h2", "h3", "h4", "h5", "h6", "i", "img", "li", "ol", "p", "pre", "q", "small",
"strike", "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul");
addAttributes("a", "href", "title", "target");
addAttributes("blockquote", "cite");
addAttributes("col", "span");
addAttributes("colgroup", "span");
addAttributes("img", "align", "alt", "src", "title");
addAttributes("ol", "start");
addAttributes("q", "cite");
addAttributes("table", "summary");
addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width");
addAttributes("th", "abbr", "axis", "colspan", "rowspan", "scope", "width");
addAttributes("video", "src", "autoplay", "controls", "loop", "muted", "poster", "preload");
addAttributes("object", "width", "height", "classid", "codebase");
addAttributes("param", "name", "value");
addAttributes("embed", "src", "quality", "width", "height", "allowFullScreen", "allowScriptAccess", "flashvars", "name", "type", "pluginspage");
addAttributes(":all", "class", "style", "height", "width", "type", "id", "name");
addProtocols("blockquote", "cite", "http", "https");
addProtocols("cite", "cite", "http", "https");
addProtocols("q", "cite", "http", "https");
// 如果添加以下的协议,那么href 必须是http、 https 等开头,相对路径则被过滤掉了
// addProtocols("a", "href", "ftp", "http", "https", "mailto", "tel");
// 如果添加以下的协议,那么src必须是http 或者 https 开头,相对路径则被过滤掉了,
// 所以必须注释掉,允许相对路径的图片资源
// addProtocols("img", "src", "http", "https");
}
@Override
public boolean isSafeAttribute(String tagName, Element el, Attribute attr) {
//不允许 javascript 开头的 src 和 href
if ("src".equalsIgnoreCase(attr.getKey()) || "href".equalsIgnoreCase(attr.getKey())) {
String value = attr.getValue();
if (StringUtils.hasText(value) && value.toLowerCase().startsWith("javascript")) {
return false;
}
}
//允许 base64 的图片内容
if ("img".equals(tagName) && "src".equals(attr.getKey()) && attr.getValue().startsWith("data:;base64")) {
return true;
}
return super.isSafeAttribute(tagName, el, attr);
}
}
}
在Java Web中应用
在Java Web中,这里不做详细介绍,只简单介绍一下思路。为了实现XSS检测与过滤,可以自定义一个拦截器,拦截需要检测的HTTP请求,并提取请求中的请求参数。如果为GET请求,可以提取URL中的查询参数,如果为POST请求,可以提取HTTP请求体中的参数。在提取到到参数后,根据是检测还是过滤需求,对请求参数进行处理。在Spring项目中,可以通过重写HttpServletRequestWrapper
类中的读取HTTP请求参数的方法实现XSS处理。当然,也可以在Web参数绑定时,通过WebDataBinder
注入自定义的字符串类型编辑器实现表单参数的处理,对于JSON参数,可以通过JsonDeserializer
在反序列化时处理。