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>
|
<artifactId>commons-lang3</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 富文本白名单净化 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
|
||||||
|
<artifactId>owasp-java-html-sanitizer</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- JSON工具类 -->
|
<!-- JSON工具类 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
@@ -196,6 +202,12 @@
|
|||||||
<artifactId>mapstruct-processor</artifactId>
|
<artifactId>mapstruct-processor</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</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.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.fastbee.common.utils.DateUtils;
|
import com.fastbee.common.utils.DateUtils;
|
||||||
|
import com.fastbee.common.utils.html.RichTextSanitizer;
|
||||||
import com.fastbee.iot.mapper.NewsCategoryMapper;
|
import com.fastbee.iot.mapper.NewsCategoryMapper;
|
||||||
import com.fastbee.iot.model.CategoryNews;
|
import com.fastbee.iot.model.CategoryNews;
|
||||||
import com.fastbee.iot.model.IdAndName;
|
import com.fastbee.iot.model.IdAndName;
|
||||||
@@ -88,6 +89,7 @@ public class NewsServiceImpl implements INewsService
|
|||||||
@Override
|
@Override
|
||||||
public int insertNews(News news)
|
public int insertNews(News news)
|
||||||
{
|
{
|
||||||
|
news.setContent(RichTextSanitizer.sanitize(news.getContent()));
|
||||||
news.setCreateTime(DateUtils.getNowDate());
|
news.setCreateTime(DateUtils.getNowDate());
|
||||||
return newsMapper.insertNews(news);
|
return newsMapper.insertNews(news);
|
||||||
}
|
}
|
||||||
@@ -101,6 +103,7 @@ public class NewsServiceImpl implements INewsService
|
|||||||
@Override
|
@Override
|
||||||
public int updateNews(News news)
|
public int updateNews(News news)
|
||||||
{
|
{
|
||||||
|
news.setContent(RichTextSanitizer.sanitize(news.getContent()));
|
||||||
news.setUpdateTime(DateUtils.getNowDate());
|
news.setUpdateTime(DateUtils.getNowDate());
|
||||||
return newsMapper.updateNews(news);
|
return newsMapper.updateNews(news);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
update news
|
update news
|
||||||
<trim prefix="SET" suffixOverrides=",">
|
<trim prefix="SET" suffixOverrides=",">
|
||||||
<if test="title != null and title != ''">title = #{title},</if>
|
<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="imgUrl != null and imgUrl != ''">img_url = #{imgUrl},</if>
|
||||||
<if test="isTop != null">is_top = #{isTop},</if>
|
<if test="isTop != null">is_top = #{isTop},</if>
|
||||||
<if test="isBanner != null">is_banner = #{isBanner},</if>
|
<if test="isBanner != null">is_banner = #{isBanner},</if>
|
||||||
@@ -124,4 +124,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
#{newsId}
|
#{newsId}
|
||||||
</foreach>
|
</foreach>
|
||||||
</delete>
|
</delete>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.fastbee.system.service.impl;
|
package com.fastbee.system.service.impl;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import com.fastbee.common.utils.html.RichTextSanitizer;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import com.fastbee.system.domain.SysNotice;
|
import com.fastbee.system.domain.SysNotice;
|
||||||
@@ -51,6 +52,7 @@ public class SysNoticeServiceImpl implements ISysNoticeService
|
|||||||
@Override
|
@Override
|
||||||
public int insertNotice(SysNotice notice)
|
public int insertNotice(SysNotice notice)
|
||||||
{
|
{
|
||||||
|
notice.setNoticeContent(RichTextSanitizer.sanitize(notice.getNoticeContent()));
|
||||||
return noticeMapper.insertNotice(notice);
|
return noticeMapper.insertNotice(notice);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +65,7 @@ public class SysNoticeServiceImpl implements ISysNoticeService
|
|||||||
@Override
|
@Override
|
||||||
public int updateNotice(SysNotice notice)
|
public int updateNotice(SysNotice notice)
|
||||||
{
|
{
|
||||||
|
notice.setNoticeContent(RichTextSanitizer.sanitize(notice.getNoticeContent()));
|
||||||
return noticeMapper.updateNotice(notice);
|
return noticeMapper.updateNotice(notice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
<liteflow.version>2.12.2</liteflow.version>
|
<liteflow.version>2.12.2</liteflow.version>
|
||||||
<commons.text.version>1.10.0</commons.text.version>
|
<commons.text.version>1.10.0</commons.text.version>
|
||||||
<paho.mqtt.version>1.2.5</paho.mqtt.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>
|
<druid.version>1.2.23</druid.version>
|
||||||
<pagehelper.boot.version>1.4.7</pagehelper.boot.version>
|
<pagehelper.boot.version>1.4.7</pagehelper.boot.version>
|
||||||
@@ -239,6 +240,13 @@
|
|||||||
<version>${commons.text.version}</version>
|
<version>${commons.text.version}</version>
|
||||||
</dependency>
|
</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工具 -->
|
<!-- excel工具 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.poi</groupId>
|
<groupId>org.apache.poi</groupId>
|
||||||
|
|||||||
Reference in New Issue
Block a user