From fecc17f9f32804012ac3553d91f1598a6dea4202 Mon Sep 17 00:00:00 2001
From: b2baccline <23131013+b2baccline@users.noreply.github.com>
Date: Fri, 27 Aug 2021 21:30:56 +0800
Subject: [PATCH] =?UTF-8?q?:zap:=20=E6=8A=BD=E8=B1=A1=E5=87=BA=20XssCleane?=
=?UTF-8?q?r=20=E8=A7=92=E8=89=B2=EF=BC=8C=E7=94=A8=E4=BA=8E=E6=8E=A7?=
=?UTF-8?q?=E5=88=B6=20Xss=20=E6=96=87=E6=9C=AC=E7=9A=84=E6=B8=85=E9=99=A4?=
=?UTF-8?q?=E8=A1=8C=E4=B8=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../hccake/ballcat/common/util/HtmlUtils.java | 47 -----------
.../common/xss/XssAutoConfiguration.java | 21 ++++-
.../common/xss/cleaner/JsoupXssCleaner.java | 83 +++++++++++++++++++
.../common/xss/cleaner/XssCleaner.java | 17 ++++
.../ballcat/common/xss/core/XssFilter.java | 5 +-
.../common/xss/core/XssRequestWrapper.java | 19 +++--
.../xss/core/XssStringJsonDeserializer.java | 10 ++-
.../xss/core/XssStringJsonSerializer.java | 10 ++-
8 files changed, 148 insertions(+), 64 deletions(-)
create mode 100644 ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/cleaner/JsoupXssCleaner.java
create mode 100644 ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/cleaner/XssCleaner.java
diff --git a/ballcat-common/ballcat-common-util/src/main/java/com/hccake/ballcat/common/util/HtmlUtils.java b/ballcat-common/ballcat-common-util/src/main/java/com/hccake/ballcat/common/util/HtmlUtils.java
index d08b639c..fcd638c0 100644
--- a/ballcat-common/ballcat-common-util/src/main/java/com/hccake/ballcat/common/util/HtmlUtils.java
+++ b/ballcat-common/ballcat-common-util/src/main/java/com/hccake/ballcat/common/util/HtmlUtils.java
@@ -3,7 +3,6 @@ package com.hccake.ballcat.common.util;
import cn.hutool.core.util.StrUtil;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
-import org.jsoup.safety.Whitelist;
/**
* @author Hccake 2020/12/21
@@ -14,27 +13,6 @@ public final class HtmlUtils {
private HtmlUtils() {
}
- private static final Whitelist WHITELIST = Whitelist.relaxed();
-
- static {
- // 富文本编辑时一些样式是使用 style 来进行实现的
- // 比如红色字体 style="color:red;", 所以需要给所有标签添加 style 属性
- // 注意:style 属性会有注入风险
- WHITELIST.addAttributes(":all", "style", "class");
- // 保留 a 标签的 target 属性
- WHITELIST.addAttributes("a", "target");
- // 支持img 为base64
- WHITELIST.addProtocols("img", "src", "data");
-
- // 保留相对路径, 保留相对路径时,必须提供对应的 baseUri 属性,否则依然会被删除
- // WHITELIST.preserveRelativeLinks(false);
-
- // 移除 a 标签和 img 标签的一些协议限制,这会导致 xss 防注入失效,如
- // 虽然可以重写 WhiteList#isSafeAttribute 来处理,但是有隐患,所以暂时不支持相对路径
- // WHITELIST.removeProtocols("a", "href", "ftp", "http", "https", "mailto");
- // WHITELIST.removeProtocols("img", "src", "http", "https");
- }
-
/**
* html 转字符串,保留换行样式
* @link https://stackoverflow.com/questions/5640334/how-do-i-preserve-line-breaks-when-using-jsoup-to-convert-html-to-plain-text
@@ -74,29 +52,4 @@ public final class HtmlUtils {
return toText(html, true);
}
- /**
- *
- * 清理不安全的 Html 标签。保留换行符
- *
- * 白名单配置参见:{@link HtmlUtils#WHITELIST}
- * @see Whitelist#relaxed()
- * @param bodyHtml HTML 文本
- * @return 清理后的 HTML 文本
- */
- public static String cleanUnSafe(String bodyHtml) {
- return cleanUnSafe(bodyHtml, WHITELIST);
- }
-
- /**
- *
- * 清理不安全的 Html 标签。保留换行符
- *
- * @param bodyHtml HTML 文本
- * @param whitelist 白名单配置
- * @return 清理后的 HTML 文本
- */
- public static String cleanUnSafe(String bodyHtml, Whitelist whitelist) {
- return Jsoup.clean(bodyHtml, "", whitelist, new Document.OutputSettings().prettyPrint(false));
- }
-
}
diff --git a/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/XssAutoConfiguration.java b/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/XssAutoConfiguration.java
index 0944bdbb..6bf169b5 100644
--- a/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/XssAutoConfiguration.java
+++ b/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/XssAutoConfiguration.java
@@ -1,6 +1,8 @@
package com.hccake.ballcat.common.xss;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.hccake.ballcat.common.xss.cleaner.JsoupXssCleaner;
+import com.hccake.ballcat.common.xss.cleaner.XssCleaner;
import com.hccake.ballcat.common.xss.config.XssProperties;
import com.hccake.ballcat.common.xss.core.XssFilter;
import com.hccake.ballcat.common.xss.core.XssStringJsonDeserializer;
@@ -26,15 +28,26 @@ import org.springframework.context.annotation.Configuration;
@ConditionalOnProperty(prefix = XssProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public class XssAutoConfiguration {
+ /**
+ * Xss 清理者
+ * @return XssCleaner
+ */
+ @ConditionalOnMissingBean(XssCleaner.class)
+ @Bean
+ public XssCleaner xssCleaner() {
+ return new JsoupXssCleaner();
+ }
+
/**
* 主要用于过滤 QueryString, Header 以及 form 中的参数
* @param xssProperties 安全配置类
* @return FilterRegistrationBean
*/
@Bean
- public FilterRegistrationBean xssFilterRegistrationBean(XssProperties xssProperties) {
+ public FilterRegistrationBean xssFilterRegistrationBean(XssProperties xssProperties,
+ XssCleaner xssCleaner) {
log.debug("XSS 过滤已开启====");
- XssFilter xssFilter = new XssFilter(xssProperties);
+ XssFilter xssFilter = new XssFilter(xssProperties, xssCleaner);
FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(xssFilter);
registrationBean.setOrder(-1);
return registrationBean;
@@ -47,9 +60,9 @@ public class XssAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "xssJacksonCustomizer")
@ConditionalOnBean(ObjectMapper.class)
- public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer() {
+ public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssCleaner xssCleaner) {
// 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理
- return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer());
+ return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(xssCleaner));
}
}
diff --git a/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/cleaner/JsoupXssCleaner.java b/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/cleaner/JsoupXssCleaner.java
new file mode 100644
index 00000000..b5f454db
--- /dev/null
+++ b/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/cleaner/JsoupXssCleaner.java
@@ -0,0 +1,83 @@
+package com.hccake.ballcat.common.xss.cleaner;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.safety.Safelist;
+
+/**
+ * @author hccake
+ */
+public class JsoupXssCleaner implements XssCleaner {
+
+ private final Safelist safelist;
+
+ /**
+ * 用于在 src 属性使用相对路径时,强制转换为绝对路径。 为空时不处理,值应为绝对路径的前缀(包含协议部分)
+ */
+ private final String baseUri;
+
+ /**
+ * 无参构造,默认使用 {@link JsoupXssCleaner#buildSafelist} 方法构建一个安全列表
+ */
+ public JsoupXssCleaner() {
+ this.safelist = buildSafelist();
+ this.baseUri = "";
+ }
+
+ public JsoupXssCleaner(Safelist safelist) {
+ this.safelist = safelist;
+ this.baseUri = "";
+ }
+
+ public JsoupXssCleaner(String baseUri) {
+ this.safelist = buildSafelist();
+ this.baseUri = baseUri;
+ }
+
+ public JsoupXssCleaner(Safelist safelist, String baseUri) {
+ this.safelist = safelist;
+ this.baseUri = baseUri;
+ }
+
+ /**
+ *
+ * 构建一个 Xss 清理的 Safelist 规则。
+ *
+ *
+ *
+ * 基于 Safelist#relaxed() 的基础上:
+ * - 扩展支持了 style 和 class 属性
+ * - a 标签额外支持了 target 属性
+ * - img 标签额外支持了 data 协议,便于支持 base64
+ *
+ * @return Safelist
+ */
+ protected Safelist buildSafelist() {
+ // 使用 jsoup 提供的默认的
+ Safelist relaxedSafelist = Safelist.relaxed();
+ // 富文本编辑时一些样式是使用 style 来进行实现的
+ // 比如红色字体 style="color:red;", 所以需要给所有标签添加 style 属性
+ // 注意:style 属性会有注入风险
+ relaxedSafelist.addAttributes(":all", "style", "class");
+ // 保留 a 标签的 target 属性
+ relaxedSafelist.addAttributes("a", "target");
+ // 支持img 为base64
+ relaxedSafelist.addProtocols("img", "src", "data");
+
+ // 保留相对路径, 保留相对路径时,必须提供对应的 baseUri 属性,否则依然会被删除
+ // WHITELIST.preserveRelativeLinks(false);
+
+ // 移除 a 标签和 img 标签的一些协议限制,这会导致 xss 防注入失效,如
+ // 虽然可以重写 WhiteList#isSafeAttribute 来处理,但是有隐患,所以暂时不支持相对路径
+ // WHITELIST.removeProtocols("a", "href", "ftp", "http", "https", "mailto");
+ // WHITELIST.removeProtocols("img", "src", "http", "https");
+
+ return relaxedSafelist;
+ }
+
+ @Override
+ public String clean(String html) {
+ return Jsoup.clean(html, baseUri, safelist, new Document.OutputSettings().prettyPrint(false));
+ }
+
+}
diff --git a/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/cleaner/XssCleaner.java b/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/cleaner/XssCleaner.java
new file mode 100644
index 00000000..b04ce461
--- /dev/null
+++ b/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/cleaner/XssCleaner.java
@@ -0,0 +1,17 @@
+package com.hccake.ballcat.common.xss.cleaner;
+
+/**
+ * 对 html 文本中的有 Xss 风险的数据进行清理
+ *
+ * @author hccake
+ */
+public interface XssCleaner {
+
+ /**
+ * 清理有 Xss 风险的文本
+ * @param html 原 html
+ * @return 清理后的 html
+ */
+ String clean(String html);
+
+}
diff --git a/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/core/XssFilter.java b/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/core/XssFilter.java
index a360b17e..5bff7b33 100644
--- a/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/core/XssFilter.java
+++ b/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/core/XssFilter.java
@@ -1,6 +1,7 @@
package com.hccake.ballcat.common.xss.core;
import cn.hutool.core.util.StrUtil;
+import com.hccake.ballcat.common.xss.cleaner.XssCleaner;
import com.hccake.ballcat.common.xss.config.XssProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.util.AntPathMatcher;
@@ -25,6 +26,8 @@ public class XssFilter extends OncePerRequestFilter {
*/
private final XssProperties xssProperties;
+ private final XssCleaner xssCleaner;
+
/**
* AntPath规则匹配器
*/
@@ -47,7 +50,7 @@ public class XssFilter extends OncePerRequestFilter {
// 开启 Xss 过滤状态
XssStateHolder.open();
try {
- filterChain.doFilter(new XssRequestWrapper(request), response);
+ filterChain.doFilter(new XssRequestWrapper(request, xssCleaner), response);
}
finally {
// 必须删除 ThreadLocal 存储的状态
diff --git a/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/core/XssRequestWrapper.java b/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/core/XssRequestWrapper.java
index 547b2002..fca0ae0d 100644
--- a/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/core/XssRequestWrapper.java
+++ b/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/core/XssRequestWrapper.java
@@ -1,6 +1,6 @@
package com.hccake.ballcat.common.xss.core;
-import com.hccake.ballcat.common.util.HtmlUtils;
+import com.hccake.ballcat.common.xss.cleaner.XssCleaner;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletRequest;
@@ -16,8 +16,11 @@ import java.util.Map;
@Slf4j
public class XssRequestWrapper extends HttpServletRequestWrapper {
- public XssRequestWrapper(HttpServletRequest request) {
+ private final XssCleaner xssCleaner;
+
+ public XssRequestWrapper(HttpServletRequest request, XssCleaner xssCleaner) {
super(request);
+ this.xssCleaner = xssCleaner;
}
@Override
@@ -27,7 +30,7 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
for (Map.Entry entry : parameters.entrySet()) {
String[] values = entry.getValue();
for (int i = 0; i < values.length; i++) {
- values[i] = HtmlUtils.cleanUnSafe(values[i]);
+ values[i] = xssCleaner.clean(values[i]);
}
map.put(entry.getKey(), values);
}
@@ -43,7 +46,7 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
int count = values.length;
String[] encodedValues = new String[count];
for (int i = 0; i < count; i++) {
- encodedValues[i] = HtmlUtils.cleanUnSafe(values[i]);
+ encodedValues[i] = xssCleaner.clean(values[i]);
}
return encodedValues;
}
@@ -54,14 +57,14 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
if (value == null) {
return null;
}
- return HtmlUtils.cleanUnSafe(value);
+ return xssCleaner.clean(value);
}
@Override
public Object getAttribute(String name) {
Object value = super.getAttribute(name);
if (value instanceof String) {
- HtmlUtils.cleanUnSafe((String) value);
+ xssCleaner.clean((String) value);
}
return value;
}
@@ -72,7 +75,7 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
if (value == null) {
return null;
}
- return HtmlUtils.cleanUnSafe(value);
+ return xssCleaner.clean(value);
}
@Override
@@ -81,7 +84,7 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
if (value == null) {
return null;
}
- return HtmlUtils.cleanUnSafe(value);
+ return xssCleaner.clean(value);
}
}
diff --git a/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/core/XssStringJsonDeserializer.java b/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/core/XssStringJsonDeserializer.java
index eb76acb3..f2e20f06 100644
--- a/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/core/XssStringJsonDeserializer.java
+++ b/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/core/XssStringJsonDeserializer.java
@@ -3,7 +3,7 @@ package com.hccake.ballcat.common.xss.core;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
-import com.hccake.ballcat.common.util.HtmlUtils;
+import com.hccake.ballcat.common.xss.cleaner.XssCleaner;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
@@ -16,6 +16,12 @@ import java.io.IOException;
@Slf4j
public class XssStringJsonDeserializer extends JsonDeserializer {
+ private final XssCleaner xssCleaner;
+
+ public XssStringJsonDeserializer(XssCleaner xssCleaner) {
+ this.xssCleaner = xssCleaner;
+ }
+
@Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String value = p.getValueAsString();
@@ -23,7 +29,7 @@ public class XssStringJsonDeserializer extends JsonDeserializer {
if (!XssStateHolder.enabled()) {
return value;
}
- return value != null ? HtmlUtils.cleanUnSafe(value) : null;
+ return value != null ? xssCleaner.clean(value) : null;
}
@Override
diff --git a/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/core/XssStringJsonSerializer.java b/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/core/XssStringJsonSerializer.java
index 52e718c5..0e80c860 100644
--- a/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/core/XssStringJsonSerializer.java
+++ b/ballcat-starters/ballcat-spring-boot-starter-xss/src/main/java/com/hccake/ballcat/common/xss/core/XssStringJsonSerializer.java
@@ -3,7 +3,7 @@ package com.hccake.ballcat.common.xss.core;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
-import com.hccake.ballcat.common.util.HtmlUtils;
+import com.hccake.ballcat.common.xss.cleaner.XssCleaner;
import java.io.IOException;
@@ -14,6 +14,12 @@ import java.io.IOException;
*/
public class XssStringJsonSerializer extends JsonSerializer {
+ private final XssCleaner xssCleaner;
+
+ public XssStringJsonSerializer(XssCleaner xssCleaner) {
+ this.xssCleaner = xssCleaner;
+ }
+
@Override
public Class handledType() {
return String.class;
@@ -25,7 +31,7 @@ public class XssStringJsonSerializer extends JsonSerializer {
if (value != null) {
// 开启 Xss 才进行处理
if (XssStateHolder.enabled()) {
- value = HtmlUtils.cleanUnSafe(value);
+ value = xssCleaner.clean(value);
}
jsonGenerator.writeString(value);
}