mirror of
https://gitee.com/beecue/fastbee.git
synced 2026-09-20 20:02:06 +08:00
fix(漏洞): 新闻、通知加xss过滤
This commit is contained in:
@@ -65,6 +65,12 @@
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 富文本白名单净化 -->
|
||||
<dependency>
|
||||
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
|
||||
<artifactId>owasp-java-html-sanitizer</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON工具类 -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
@@ -196,6 +202,12 @@
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user