fix(漏洞): 新闻、通知加xss过滤

This commit is contained in:
ZhangZhiYi
2026-08-13 11:42:51 +08:00
parent a66e9e1441
commit 284b714c1e
7 changed files with 166 additions and 2 deletions

View File

@@ -0,0 +1,63 @@
package com.fastbee.common.utils.html;
import java.util.regex.Pattern;
import org.owasp.html.HtmlPolicyBuilder;
import org.owasp.html.PolicyFactory;
/**
* 富文本白名单净化工具。
*
* <p>保留 Quill 编辑器常用的安全格式,删除脚本、事件属性、危险协议和未授权标签。</p>
*/
public final class RichTextSanitizer
{
private static final Pattern IMAGE_DIMENSION_PATTERN = Pattern.compile("[0-9]{1,5}");
private static final Pattern LINK_TARGET_PATTERN = Pattern.compile("(?i)_(?:blank|self)");
/** Quill 输出的安全样式类,禁止通过任意 class 扩大前端样式能力。 */
private static final Pattern QUIL_CLASS_PATTERN = Pattern.compile(
"(?i)(?:ql-(?:align-(?:center|right|justify)|indent-[1-8]|size-(?:small|large|huge)|"
+ "direction-rtl|font-(?:serif|monospace)|syntax))"
+ "(?:\\s+ql-(?:align-(?:center|right|justify)|indent-[1-8]|size-(?:small|large|huge)|"
+ "direction-rtl|font-(?:serif|monospace)|syntax))*");
/**
* 策略对象创建成本较高且本身不可变,所有请求复用同一实例。
* 不允许 iframe、video、object、embed、svg 等主动内容。
*/
private static final PolicyFactory POLICY = new HtmlPolicyBuilder()
.allowCommonInlineFormattingElements()
.allowCommonBlockElements()
.allowStyling()
.allowUrlProtocols("http", "https")
.allowElements("a", "img", "br", "pre", "div", "span")
.allowWithoutAttributes("a", "br", "span")
.allowAttributes("href").onElements("a")
.allowAttributes("target").matching(LINK_TARGET_PATTERN).onElements("a")
.allowAttributes("alt", "src").onElements("img")
.allowAttributes("height", "width").matching(IMAGE_DIMENSION_PATTERN).onElements("img")
.allowAttributes("class")
.matching(QUIL_CLASS_PATTERN)
.onElements("p", "div", "pre", "ol", "ul", "li", "span")
.requireRelNofollowOnLinks()
.toFactory();
private RichTextSanitizer()
{
}
/**
* 净化不可信富文本。
*
* @param html 待净化的富文本
* @return 可安全嵌入 HTML 正文上下文的富文本
*/
public static String sanitize(String html)
{
if (html == null || html.isEmpty())
{
return html;
}
return POLICY.sanitize(html);
}
}

View File

@@ -0,0 +1,75 @@
package com.fastbee.common.utils.html;
import org.junit.Assert;
import org.junit.Test;
/**
* 富文本白名单净化测试。
*/
public class RichTextSanitizerTest
{
@Test
public void shouldRemoveExecutableContent()
{
String input = "<p onclick=\"alert(1)\">正文<script>alert(2)</script>"
+ "<img src=\"/profile/a.png\" onerror=\"alert(3)\"></p>";
String sanitized = RichTextSanitizer.sanitize(input);
Assert.assertTrue(sanitized.contains("正文"));
Assert.assertFalse(sanitized.contains("<script"));
Assert.assertFalse(sanitized.contains("onclick"));
Assert.assertFalse(sanitized.contains("onerror"));
}
@Test
public void shouldRemoveDangerousUrlsAndActiveElements()
{
String input = "<a href=\"javascript:alert(1)\">危险链接</a>"
+ "<iframe src=\"https://example.com\"></iframe>"
+ "<svg onload=\"alert(2)\"></svg>"
+ "<span style=\"background-image:url(javascript:alert(3))\">危险样式</span>";
String sanitized = RichTextSanitizer.sanitize(input);
Assert.assertTrue(sanitized.contains("危险链接"));
Assert.assertFalse(sanitized.contains("javascript:"));
Assert.assertFalse(sanitized.contains("<iframe"));
Assert.assertFalse(sanitized.contains("<svg"));
Assert.assertFalse(sanitized.contains("background-image"));
}
@Test
public void shouldPreserveCommonQuillFormatting()
{
String input = "<h2>标题</h2><p class=\"ql-align-center\"><strong>正文</strong>"
+ "<span style=\"color: rgb(230, 0, 0);\">红色</span></p>"
+ "<ol><li class=\"ql-indent-1\">条目</li></ol>"
+ "<p><a href=\"https://example.com\" target=\"_blank\">链接</a>"
+ "<img src=\"/profile/a.png\" alt=\"图片\"></p>";
String sanitized = RichTextSanitizer.sanitize(input);
Assert.assertTrue(sanitized.contains("<h2>标题</h2>"));
Assert.assertTrue(sanitized.contains("ql-align-center"));
Assert.assertTrue(sanitized.contains("<strong>正文</strong>"));
Assert.assertTrue(sanitized.contains("红色"));
Assert.assertTrue(sanitized.contains("<ol>"));
Assert.assertTrue(sanitized.contains("ql-indent-1"));
Assert.assertTrue(sanitized.contains("https://example.com"));
Assert.assertTrue(sanitized.contains("noopener"));
Assert.assertTrue(sanitized.contains("noreferrer"));
Assert.assertTrue(sanitized.contains("/profile/a.png"));
}
@Test
public void shouldBeNullSafeAndIdempotent()
{
Assert.assertNull(RichTextSanitizer.sanitize(null));
Assert.assertEquals("", RichTextSanitizer.sanitize(""));
Assert.assertEquals("", RichTextSanitizer.sanitize("<script>alert(1)</script>"));
String sanitized = RichTextSanitizer.sanitize("<p><em>正文</em></p>");
Assert.assertEquals(sanitized, RichTextSanitizer.sanitize(sanitized));
}
}