From 284b714c1e669d3a45611ee738dcd7cbe84a8d92 Mon Sep 17 00:00:00 2001 From: ZhangZhiYi <1667783625@qq.com> Date: Thu, 13 Aug 2026 11:42:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E6=BC=8F=E6=B4=9E):=20=E6=96=B0=E9=97=BB?= =?UTF-8?q?=E3=80=81=E9=80=9A=E7=9F=A5=E5=8A=A0xss=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- springboot/fastbee-common/pom.xml | 12 +++ .../common/utils/html/RichTextSanitizer.java | 63 ++++++++++++++++ .../utils/html/RichTextSanitizerTest.java | 75 +++++++++++++++++++ .../iot/service/impl/NewsServiceImpl.java | 3 + .../main/resources/mapper/iot/NewsMapper.xml | 4 +- .../service/impl/SysNoticeServiceImpl.java | 3 + springboot/pom.xml | 8 ++ 7 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 springboot/fastbee-common/src/main/java/com/fastbee/common/utils/html/RichTextSanitizer.java create mode 100644 springboot/fastbee-common/src/test/java/com/fastbee/common/utils/html/RichTextSanitizerTest.java diff --git a/springboot/fastbee-common/pom.xml b/springboot/fastbee-common/pom.xml index 827eaf96..5ab6f131 100644 --- a/springboot/fastbee-common/pom.xml +++ b/springboot/fastbee-common/pom.xml @@ -65,6 +65,12 @@ commons-lang3 + + + com.googlecode.owasp-java-html-sanitizer + owasp-java-html-sanitizer + + com.fasterxml.jackson.core @@ -196,6 +202,12 @@ mapstruct-processor + + junit + junit + test + + diff --git a/springboot/fastbee-common/src/main/java/com/fastbee/common/utils/html/RichTextSanitizer.java b/springboot/fastbee-common/src/main/java/com/fastbee/common/utils/html/RichTextSanitizer.java new file mode 100644 index 00000000..830512ca --- /dev/null +++ b/springboot/fastbee-common/src/main/java/com/fastbee/common/utils/html/RichTextSanitizer.java @@ -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; + +/** + * 富文本白名单净化工具。 + * + *

保留 Quill 编辑器常用的安全格式,删除脚本、事件属性、危险协议和未授权标签。

+ */ +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); + } +} diff --git a/springboot/fastbee-common/src/test/java/com/fastbee/common/utils/html/RichTextSanitizerTest.java b/springboot/fastbee-common/src/test/java/com/fastbee/common/utils/html/RichTextSanitizerTest.java new file mode 100644 index 00000000..eac1f9bb --- /dev/null +++ b/springboot/fastbee-common/src/test/java/com/fastbee/common/utils/html/RichTextSanitizerTest.java @@ -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 = "

正文" + + "

"; + + String sanitized = RichTextSanitizer.sanitize(input); + + Assert.assertTrue(sanitized.contains("正文")); + Assert.assertFalse(sanitized.contains("危险链接" + + "" + + "" + + "危险样式"; + + String sanitized = RichTextSanitizer.sanitize(input); + + Assert.assertTrue(sanitized.contains("危险链接")); + Assert.assertFalse(sanitized.contains("javascript:")); + Assert.assertFalse(sanitized.contains("正文" + + "红色

" + + "
  1. 条目
" + + "

链接" + + "\"图片\"

"; + + String sanitized = RichTextSanitizer.sanitize(input); + + Assert.assertTrue(sanitized.contains("

标题

")); + Assert.assertTrue(sanitized.contains("ql-align-center")); + Assert.assertTrue(sanitized.contains("正文")); + Assert.assertTrue(sanitized.contains("红色")); + Assert.assertTrue(sanitized.contains("
    ")); + 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("")); + + String sanitized = RichTextSanitizer.sanitize("

    正文

    "); + Assert.assertEquals(sanitized, RichTextSanitizer.sanitize(sanitized)); + } +} diff --git a/springboot/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/NewsServiceImpl.java b/springboot/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/NewsServiceImpl.java index 448b77ae..6ead4e26 100644 --- a/springboot/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/NewsServiceImpl.java +++ b/springboot/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/NewsServiceImpl.java @@ -3,6 +3,7 @@ package com.fastbee.iot.service.impl; import java.util.ArrayList; import java.util.List; import com.fastbee.common.utils.DateUtils; +import com.fastbee.common.utils.html.RichTextSanitizer; import com.fastbee.iot.mapper.NewsCategoryMapper; import com.fastbee.iot.model.CategoryNews; import com.fastbee.iot.model.IdAndName; @@ -88,6 +89,7 @@ public class NewsServiceImpl implements INewsService @Override public int insertNews(News news) { + news.setContent(RichTextSanitizer.sanitize(news.getContent())); news.setCreateTime(DateUtils.getNowDate()); return newsMapper.insertNews(news); } @@ -101,6 +103,7 @@ public class NewsServiceImpl implements INewsService @Override public int updateNews(News news) { + news.setContent(RichTextSanitizer.sanitize(news.getContent())); news.setUpdateTime(DateUtils.getNowDate()); return newsMapper.updateNews(news); } diff --git a/springboot/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/NewsMapper.xml b/springboot/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/NewsMapper.xml index c4eeb2a1..252cf074 100644 --- a/springboot/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/NewsMapper.xml +++ b/springboot/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/NewsMapper.xml @@ -96,7 +96,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" update news title = #{title}, - content = #{content}, + content = #{content}, img_url = #{imgUrl}, is_top = #{isTop}, is_banner = #{isBanner}, @@ -124,4 +124,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{newsId} - \ No newline at end of file + diff --git a/springboot/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysNoticeServiceImpl.java b/springboot/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysNoticeServiceImpl.java index 892ca3e2..0981dd76 100644 --- a/springboot/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysNoticeServiceImpl.java +++ b/springboot/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysNoticeServiceImpl.java @@ -1,6 +1,7 @@ package com.fastbee.system.service.impl; import java.util.List; +import com.fastbee.common.utils.html.RichTextSanitizer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.fastbee.system.domain.SysNotice; @@ -51,6 +52,7 @@ public class SysNoticeServiceImpl implements ISysNoticeService @Override public int insertNotice(SysNotice notice) { + notice.setNoticeContent(RichTextSanitizer.sanitize(notice.getNoticeContent())); return noticeMapper.insertNotice(notice); } @@ -63,6 +65,7 @@ public class SysNoticeServiceImpl implements ISysNoticeService @Override public int updateNotice(SysNotice notice) { + notice.setNoticeContent(RichTextSanitizer.sanitize(notice.getNoticeContent())); return noticeMapper.updateNotice(notice); } diff --git a/springboot/pom.xml b/springboot/pom.xml index c3ef4252..412f413e 100644 --- a/springboot/pom.xml +++ b/springboot/pom.xml @@ -50,6 +50,7 @@ 2.12.2 1.10.0 1.2.5 + 20260313.1 1.2.23 1.4.7 @@ -239,6 +240,13 @@ ${commons.text.version} + + + com.googlecode.owasp-java-html-sanitizer + owasp-java-html-sanitizer + ${owasp-java-html-sanitizer.version} + + org.apache.poi