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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
update news
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="title != null and title != ''">title = #{title},</if>
|
||||
<if test="content != null and content != ''">content = #{content},</if>
|
||||
<if test="content != null">content = #{content},</if>
|
||||
<if test="imgUrl != null and imgUrl != ''">img_url = #{imgUrl},</if>
|
||||
<if test="isTop != null">is_top = #{isTop},</if>
|
||||
<if test="isBanner != null">is_banner = #{isBanner},</if>
|
||||
@@ -124,4 +124,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
#{newsId}
|
||||
</foreach>
|
||||
</delete>
|
||||
</mapper>
|
||||
</mapper>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
<liteflow.version>2.12.2</liteflow.version>
|
||||
<commons.text.version>1.10.0</commons.text.version>
|
||||
<paho.mqtt.version>1.2.5</paho.mqtt.version>
|
||||
<owasp-java-html-sanitizer.version>20260313.1</owasp-java-html-sanitizer.version>
|
||||
|
||||
<druid.version>1.2.23</druid.version>
|
||||
<pagehelper.boot.version>1.4.7</pagehelper.boot.version>
|
||||
@@ -239,6 +240,13 @@
|
||||
<version>${commons.text.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 富文本白名单净化 -->
|
||||
<dependency>
|
||||
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
|
||||
<artifactId>owasp-java-html-sanitizer</artifactId>
|
||||
<version>${owasp-java-html-sanitizer.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- excel工具 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
|
||||
Reference in New Issue
Block a user