抽象出 XssCleaner 角色,用于控制 Xss 文本的清除行为

This commit is contained in:
b2baccline
2021-08-27 21:30:56 +08:00
parent 3cc121d06d
commit fecc17f9f3
8 changed files with 148 additions and 64 deletions

View File

@@ -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 属性会有注入风险 <img STYLE="background-image:url(javascript:alert('XSS'))">
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 防注入失效,如 <img src=javascript:alert("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);
}
/**
* <p>
* 清理不安全的 Html 标签。保留换行符
* </p>
* 白名单配置参见:{@link HtmlUtils#WHITELIST}
* @see Whitelist#relaxed()
* @param bodyHtml HTML 文本
* @return 清理后的 HTML 文本
*/
public static String cleanUnSafe(String bodyHtml) {
return cleanUnSafe(bodyHtml, WHITELIST);
}
/**
* <p>
* 清理不安全的 Html 标签。保留换行符
* </p>
* @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));
}
}

View File

@@ -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<XssFilter> xssFilterRegistrationBean(XssProperties xssProperties) {
public FilterRegistrationBean<XssFilter> xssFilterRegistrationBean(XssProperties xssProperties,
XssCleaner xssCleaner) {
log.debug("XSS 过滤已开启====");
XssFilter xssFilter = new XssFilter(xssProperties);
XssFilter xssFilter = new XssFilter(xssProperties, xssCleaner);
FilterRegistrationBean<XssFilter> 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));
}
}

View File

@@ -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;
}
/**
* <p>
* 构建一个 Xss 清理的 Safelist 规则。
* </p>
*
* <ul>
* 基于 Safelist#relaxed() 的基础上:
* <li>扩展支持了 style 和 class 属性</li>
* <li>a 标签额外支持了 target 属性</li>
* <li>img 标签额外支持了 data 协议,便于支持 base64</li>
* </ul>
* @return Safelist
*/
protected Safelist buildSafelist() {
// 使用 jsoup 提供的默认的
Safelist relaxedSafelist = Safelist.relaxed();
// 富文本编辑时一些样式是使用 style 来进行实现的
// 比如红色字体 style="color:red;", 所以需要给所有标签添加 style 属性
// 注意style 属性会有注入风险 <img STYLE="background-image:url(javascript:alert('XSS'))">
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 防注入失效,如 <img src=javascript:alert("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));
}
}

View File

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

View File

@@ -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 存储的状态

View File

@@ -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<String, String[]> 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);
}
}

View File

@@ -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<String> {
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<String> {
if (!XssStateHolder.enabled()) {
return value;
}
return value != null ? HtmlUtils.cleanUnSafe(value) : null;
return value != null ? xssCleaner.clean(value) : null;
}
@Override

View File

@@ -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<String> {
private final XssCleaner xssCleaner;
public XssStringJsonSerializer(XssCleaner xssCleaner) {
this.xssCleaner = xssCleaner;
}
@Override
public Class<String> handledType() {
return String.class;
@@ -25,7 +31,7 @@ public class XssStringJsonSerializer extends JsonSerializer<String> {
if (value != null) {
// 开启 Xss 才进行处理
if (XssStateHolder.enabled()) {
value = HtmlUtils.cleanUnSafe(value);
value = xssCleaner.clean(value);
}
jsonGenerator.writeString(value);
}