过滤 HTML 代码以防止 XSS 攻击的几种方案
1、简介
跨站脚本攻击(XSS)是一种安全漏洞,允许攻击者向网页应用中注入恶意脚本。这些脚本能在用户浏览器中执行,导致数据窃取、会话劫持或页面篡改等风险。
本文将带你了解如何在 Java 应用中过滤 HTML 输入以防止 XSS 攻击。
2、项目设置
首先,需要在 pom.xml
中添加 OWASP Java HTML sanitizer 库:
<dependency>
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
<artifactId>owasp-java-html-sanitizer</artifactId>
<version>20240325.1</version>
</dependency>
该库提供高度可配置的策略驱动式 Sanitizer(净化器),既能处理复杂 HTML 内容,又能有效防御 XSS 攻击。
3、实现基础版 OWASP HTML 过滤
添加依赖后,我们定义一个工具方法,利用该库清理可能非法的 HTML 输入。
以下创建了一个可复用的工具类,采用默认策略(仅允许基础格式化标签)实现 HTML 过滤:
public class HtmlSanitizerUtil {
private static final PolicyFactory POLICY = Sanitizers.FORMATTING.and(Sanitizers.LINKS);
public static String sanitize(String htmlContent) {
return POLICY.sanitize(htmlContent);
}
}
上例中,我们通过组合两个内置 Sanitizer(Sanitizers.FORMATTING
和 Sanitizers.LINKS
)配置过滤策略。该策略允许基础 HTML 格式化标签(如 <b>
、<i>
、<u>
)以及通过 <a>
标签实现的超链接。随后 sanitize()
方法将此策略应用于输入字符串,返回过滤后的 HTML 内容。
测试,通过输入不安全的 HTML 来验证是否能过滤非法字符,以确保输出仅包含允许的标签:
String input = "<script>alert('XSS')</script><b>Hello</b> <a href='https://example.com'>link</a>";
String expectedOutput = "<b>Hello</b> <a href=\"https://example.com\" rel=\"nofollow\">link</a>";
String sanitized = HtmlSanitizerUtil.sanitize(input);
assertEquals(expectedOutput, sanitized);
如上,我们传入包含恶意 <script>
标签的字符串以及合法的格式化和超链接元素。Sanitizer 会移除 script
标签并保留安全标签,同时自动为链接添加 rel="nofollow"
属性作为额外防护。
4、使用 OWASP HtmlPolicyBuilder 实现灵活过滤
尽管内置策略提供了便利性,但我们通常需要更精细地控制允许的 HTML 元素和属性。HtmlPolicyBuilder
API 通过 Fluent 式语法支持自定义策略的构建。
下面实现一个同时允许块级和行内格式化元素的 Sanitizer:
private static final PolicyFactory POLICY = new HtmlPolicyBuilder()
.allowCommonBlockElements()
.allowCommonInlineFormattingElements()
.toFactory();
public static String sanitize(String html) {
return POLICY.sanitize(html);
}
该实现创建的策略允许常见的块级元素(如 <div>
、<p>
、<ul>
和 <ol>
)以及行内元素(如 <b>
、<i>
和 <em>
)。sanitize()
方法使用此策略在保留常用布局和样式元素的同时,移除所有危险标签和属性(PolicyFactory
实例是线程安全的)。
接下来,通过基于断言的测试来验证该实现,将过滤结果与预期输出进行对比:
String input = "<div onclick='alert(1)'><p><b>Text</b></p></div><script>alert('x')</script>";
String expectedOutput = "<div><p><b>Text</b></p></div>";
String sanitized = HtmlSanitizer.sanitize(input);
assertEquals(expectedOutput, sanitized);
本例中,输入内容包含不安全的事件处理器和 <script>
标签。我们的自定义策略会移除危险属性和元素,仅保留允许的结构性和格式化标签。 这种方法在安全性与保留用户格式(适用于博客评论、CMS 内容或论坛)之间实现了良好平衡。
5、创建自定义策略
在某些应用中,我们可能需要允许不同的 HTML 元素集或更严格地限制某些属性。OWASP Java HTML Sanitizer 提供了 FLuent 式 API 来构建自定义策略。 以下是一个更复杂的策略配置示例:
public class CustomHtmlSanitizer {
private static final PolicyFactory POLICY = new HtmlPolicyBuilder()
.allowElements("a", "p", "div", "span", "h1", "h2", "h3")
.allowUrlProtocols("https")
.allowAttributes("href").onElements("a")
.requireRelNofollowOnLinks()
.allowAttributes("class").globally()
.allowStyling()
.toFactory();
public static String sanitize(String html) {
return POLICY.sanitize(html);
}
}
上例中,我们通过以下规则构建自定义过滤策略:
- 允许的元素:该策略允许结构标签(如
<div>
、<p>
和标题<h1>
至<h3>
),以及<a>
和<span>
。 - 允许的 URL 协议:仅允许
HTTPS
链接,防止不安全的 HTTP 链接导致混合内容问题。 - 链接属性:允许
<a>
标签使用href
属性,并自动为每个链接添加rel="nofollow"
属性以防止 SEO 滥用. - 全局属性:允许所有元素使用
class
属性,以支持 CSS 样式。 - 行内样式:通过
style
属性允许安全的 CSS 样式(如color
、font-weight
等无害声明)
该方法让我们能完全控制过滤后内容允许的结构和外观,同时确保有效清除所有不安全行为(如内联 JavaScript、事件处理器或未允许的协议)。
通过测试用例验证该策略:
String input = "<h1 class='title' style='color:red;'>Welcome</h1>"
+ "<a href='https://example.com' onclick='stealCookies()'>Click</a>"
+ "<script>alert('xss');</script>";
String expectedOutput =
"<h1 class=\"title\" style=\"color:red\">Welcome</h1><a href=\"https://example.com\" rel=\"nofollow\">Click</a>";
String sanitized = CustomHtmlSanitizer.sanitize(input);
assertEquals(expectedOutput, sanitized);
此类自定义策略适用于博客、论坛或 CMS 系统的用户生成内容过滤场景,能在保证安全性的前提下,兼顾必要的格式灵活性。
6、替代方案:JSoup HTML Cleaner
JSoup 是另一种常用的 Java HTML 过滤库。JSoup 提供强大的 HTML 解析和清理能力,特别适用于需要同时进行 DOM 检查和操作的场景。
首先,在 pom.xml
中添加 JSoup 依赖:
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.20.1</version>
</dependency>
添加依赖后,可实现一个定义允许的 HTML 元素和属性白名单的 Sanitizer。以下是示例实现:
public class JsoupHtmlSanitizer {
public static String sanitize(String html) {
Safelist safelist = Safelist.basic()
.addTags("h1", "h2", "h3")
.addAttributes("a", "target")
.addProtocols("a", "href", "http", "https");
return Jsoup.clean(html, safelist);
}
}
本示例中,我们基于 Safelist.basic()
(允许基础 HTML 标签如 <b>
、<i>
、<u>
和 <a>
),扩展添加了标题标签 <h1>
、<h2>
和 <h3>
的支持。
最后,还允许锚点标签使用 target
属性(支持 target="_blank"
在新标签页打开链接),并将链接协议限制为 http
和 https
。
测试:
String input = "<h1 onclick='x()'>Title</h1><a href='javascript:alert(1)' target='_blank'>Click</a>";
String expectedOutput = "<h1>Title</h1><a target=\"_blank\" rel=\"nofollow\">Click</a>";
String sanitized = JsoupHtmlSanitizer.sanitize(input);
assertEquals(expectedOutput, sanitized);
与 OWASP Sanitizer 不同,JSoup 采用更直观的 “白名单” 模式,特别适用于处理预定义的 HTML 结构,或在过滤前需提取/修改特定 HTML 节点的场景。
此外,JSoup 会自动为使用 target="_blank"
的标签添加 rel="nofollow"
防止逆向标签劫持攻击,默认提供更强的安全性。
7、总结
本文介绍了 Java 应用中防御 XSS 攻击的多种 HTML 过滤方案。
OWASP Java HTML Sanitizer 适合需要严格 XSS 防护及细粒度策略控制的场景,而 JSoup 更适用于涉及 HTML 解析、操作或仅需基于白名单的简易过滤场景。
Ref:https://www.baeldung.com/java-sanitize-html-prevent-xss-attacks