⚡ 抽象出 XssCleaner 角色,用于控制 Xss 文本的清除行为
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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 存储的状态
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user