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); }