过滤 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.FORMATTINGSanitizers.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 样式(如 colorfont-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" 在新标签页打开链接),并将链接协议限制为 httphttps

测试:

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