bean = new FilterRegistrationBean<>(filter);
bean.setOrder(order);
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/clean/JsoupXssCleaner.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/clean/JsoupXssCleaner.java
new file mode 100644
index 000000000..5a72834af
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/clean/JsoupXssCleaner.java
@@ -0,0 +1,81 @@
+package cn.iocoder.yudao.framework.web.core.clean;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.safety.Safelist;
+
+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/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/clean/XssCleaner.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/clean/XssCleaner.java
new file mode 100644
index 000000000..a5ecc1bcc
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/clean/XssCleaner.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.framework.web.core.clean;
+
+/**
+ * 对 html 文本中的有 Xss 风险的数据进行清理
+ */
+public interface XssCleaner {
+ /**
+ * 清理有 Xss 风险的文本
+ *
+ * @param html 原 html
+ * @return 清理后的 html
+ */
+ String clean(String html);
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssFilter.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssFilter.java
index 050a86cc1..2da18768d 100644
--- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssFilter.java
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssFilter.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.framework.web.core.filter;
import cn.iocoder.yudao.framework.web.config.XssProperties;
+import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
import lombok.AllArgsConstructor;
import org.springframework.util.PathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
@@ -13,7 +14,7 @@ import java.io.IOException;
/**
* Xss 过滤器
- *
+ *
* 对 Xss 不了解的胖友,可以看看 http://www.iocoder.cn/Fight/The-new-girl-asked-me-why-AJAX-requests-are-not-secure-I-did-not-answer/
*
* @author 芋道源码
@@ -30,10 +31,12 @@ public class XssFilter extends OncePerRequestFilter {
*/
private final PathMatcher pathMatcher;
+ private final XssCleaner xssCleaner;
+
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
- filterChain.doFilter(new XssRequestWrapper(request), response);
+ filterChain.doFilter(new XssRequestWrapper(request, xssCleaner), response);
}
@Override
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssRequestWrapper.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssRequestWrapper.java
index 25bd20978..1724cb163 100644
--- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssRequestWrapper.java
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssRequestWrapper.java
@@ -1,21 +1,10 @@
package cn.iocoder.yudao.framework.web.core.filter;
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.io.IoUtil;
-import cn.hutool.core.util.ArrayUtil;
-import cn.hutool.core.util.ReflectUtil;
-import cn.hutool.core.util.StrUtil;
-import cn.hutool.http.HTMLFilter;
-import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
+import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
-import javax.servlet.ReadListener;
-import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
+import java.util.LinkedHashMap;
import java.util.Map;
/**
@@ -24,113 +13,75 @@ import java.util.Map;
* @author 芋道源码
*/
public class XssRequestWrapper extends HttpServletRequestWrapper {
+ private final XssCleaner xssCleaner;
- /**
- * 基于线程级别的 HTMLFilter 对象,因为它线程非安全
- */
- private static final ThreadLocal HTML_FILTER = ThreadLocal.withInitial(() -> {
- HTMLFilter htmlFilter = new HTMLFilter();
- // 反射修改 encodeQuotes 属性为 false,避免 " 被转移成 " 字符
- ReflectUtil.setFieldValue(htmlFilter, "encodeQuotes", false);
- return htmlFilter;
- });
-
- public XssRequestWrapper(HttpServletRequest request) {
+ public XssRequestWrapper(HttpServletRequest request, XssCleaner xssCleaner) {
super(request);
+ this.xssCleaner = xssCleaner;
}
- private static String filterXss(String content) {
- if (StrUtil.isEmpty(content)) {
- return content;
+ @Override
+ public Map getParameterMap() {
+ Map map = new LinkedHashMap<>();
+ Map parameters = super.getParameterMap();
+ for (Map.Entry entry : parameters.entrySet()) {
+ String[] values = entry.getValue();
+ for (int i = 0; i < values.length; i++) {
+ values[i] = xssCleaner.clean(values[i]);
+ }
+ map.put(entry.getKey(), values);
}
- return HTML_FILTER.get().filter(content);
- }
-
- // ========== IO 流相关 ==========
-
- @Override
- public BufferedReader getReader() throws IOException {
- return new BufferedReader(new InputStreamReader(this.getInputStream()));
- }
-
- @Override
- public ServletInputStream getInputStream() throws IOException {
- // 如果非 json 请求,不进行 Xss 处理
- if (!ServletUtils.isJsonRequest(this)) {
- return super.getInputStream();
- }
-
- // 读取内容,并过滤
- String content = IoUtil.readUtf8(super.getInputStream());
- content = filterXss(content);
- final ByteArrayInputStream newInputStream = new ByteArrayInputStream(content.getBytes());
- // 返回 ServletInputStream
- return new ServletInputStream() {
-
- @Override
- public int read() {
- return newInputStream.read();
- }
-
- @Override
- public boolean isFinished() {
- return true;
- }
-
- @Override
- public boolean isReady() {
- return true;
- }
-
- @Override
- public void setReadListener(ReadListener readListener) {}
-
- };
- }
-
- // ========== Param 相关 ==========
-
- @Override
- public String getParameter(String name) {
- String value = super.getParameter(name);
- return filterXss(value);
+ return map;
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
- if (ArrayUtil.isEmpty(values)) {
- return values;
+ if (values == null) {
+ return null;
}
- // 过滤处理
- for (int i = 0; i < values.length; i++) {
- values[i] = filterXss(values[i]);
+ int count = values.length;
+ String[] encodedValues = new String[count];
+ for (int i = 0; i < count; i++) {
+ encodedValues[i] = xssCleaner.clean(values[i]);
}
- return values;
+ return encodedValues;
}
@Override
- public Map getParameterMap() {
- Map valueMap = super.getParameterMap();
- if (CollUtil.isEmpty(valueMap)) {
- return valueMap;
+ public String getParameter(String name) {
+ String value = super.getParameter(name);
+ if (value == null) {
+ return null;
}
- // 过滤处理
- for (Map.Entry entry : valueMap.entrySet()) {
- String[] values = entry.getValue();
- for (int i = 0; i < values.length; i++) {
- values[i] = filterXss(values[i]);
- }
- }
- return valueMap;
+ return xssCleaner.clean(value);
}
- // ========== Header 相关 ==========
+ @Override
+ public Object getAttribute(String name) {
+ Object value = super.getAttribute(name);
+ if (value instanceof String) {
+ xssCleaner.clean((String) value);
+ }
+ return value;
+ }
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
- return filterXss(value);
+ if (value == null) {
+ return null;
+ }
+ return xssCleaner.clean(value);
+ }
+
+ @Override
+ public String getQueryString() {
+ String value = super.getQueryString();
+ if (value == null) {
+ return null;
+ }
+ return xssCleaner.clean(value);
}
}
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/json/XssStringJsonDeserializer.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/json/XssStringJsonDeserializer.java
new file mode 100644
index 000000000..185469be5
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/json/XssStringJsonDeserializer.java
@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.framework.web.core.json;
+
+import cn.iocoder.yudao.framework.web.config.XssProperties;
+import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.deser.std.StringDeserializer;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+
+/**
+ * XSS过滤 jackson 反序列化器
+ *
+ * 参考 ballcat 实现
+ */
+@Slf4j
+@AllArgsConstructor
+public class XssStringJsonDeserializer extends StringDeserializer {
+
+ private final XssCleaner xssCleaner;
+ private final XssProperties xssProperties;
+
+ @Override
+ public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ if (p.hasToken(JsonToken.VALUE_STRING)) {
+ return getCleanText(p.getText());
+ }
+ JsonToken t = p.currentToken();
+ // [databind#381]
+ if (t == JsonToken.START_ARRAY) {
+ return _deserializeFromArray(p, ctxt);
+ }
+ // need to gracefully handle byte[] data, as base64
+ if (t == JsonToken.VALUE_EMBEDDED_OBJECT) {
+ Object ob = p.getEmbeddedObject();
+ if (ob == null) {
+ return null;
+ }
+ if (ob instanceof byte[]) {
+ return ctxt.getBase64Variant().encode((byte[]) ob, false);
+ }
+ // otherwise, try conversion using toString()...
+ return ob.toString();
+ }
+ // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML)
+ if (t == JsonToken.START_OBJECT) {
+ return ctxt.extractScalarFromObject(p, this, _valueClass);
+ }
+ // allow coercions for other scalar types
+ // 17-Jan-2018, tatu: Related to [databind#1853] avoid FIELD_NAME by ensuring it's
+ // "real" scalar
+ if (t.isScalarValue()) {
+ String text = p.getValueAsString();
+ return getCleanText(text);
+ }
+ return (String) ctxt.handleUnexpectedToken(_valueClass, p);
+ }
+
+ private String getCleanText(String text) {
+ if (text == null) {
+ return null;
+ }
+ return xssProperties.isEnable() ? xssCleaner.clean(text) : text;
+ }
+
+}
+
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/json/XssStringJsonSerializer.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/json/XssStringJsonSerializer.java
new file mode 100644
index 000000000..261c99ffe
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/json/XssStringJsonSerializer.java
@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.framework.web.core.json;
+
+import cn.iocoder.yudao.framework.web.config.XssProperties;
+import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import lombok.AllArgsConstructor;
+
+import java.io.IOException;
+
+/**
+ * XSS过滤 jackson 序列化器
+ *
+ * 参考 ballcat 实现
+ */
+@AllArgsConstructor
+public class XssStringJsonSerializer extends JsonSerializer {
+
+ private final XssCleaner xssCleaner;
+ private final XssProperties xssProperties;
+
+
+ @Override
+ public Class handledType() {
+ return String.class;
+ }
+
+ @Override
+ public void serialize(String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
+ throws IOException {
+ if (value != null) {
+ // 开启 Xss 才进行处理
+ if (xssProperties.isEnable()) {
+ value = xssCleaner.clean(value);
+ }
+ jsonGenerator.writeString(value);
+ }
+ }
+
+}
+