Commit c1939cf2add64049ff1bb77cac9a7618aee13278

Authored by 芋道源码
Committed by Gitee
2 parents 3df24e11 099754c2

!361 xss启用后编辑器上传图片错误

Merge pull request !361 from 与或非/issues/I58M67
yudao-dependencies/pom.xml
... ... @@ -41,6 +41,7 @@
41 41 <!-- Bpm 工作流相关 -->
42 42 <flowable.version>6.8.0</flowable.version>
43 43 <!-- 工具类相关 -->
  44 + <jsoup.version>1.15.3</jsoup.version>
44 45 <lombok.version>1.18.24</lombok.version>
45 46 <mapstruct.version>1.5.3.Final</mapstruct.version>
46 47 <hutool.version>5.8.11</hutool.version>
... ... @@ -522,6 +523,12 @@
522 523 <version>${ip2region.version}</version>
523 524 </dependency>
524 525  
  526 + <dependency>
  527 + <groupId>org.jsoup</groupId>
  528 + <artifactId>jsoup</artifactId>
  529 + <version>${jsoup.version}</version>
  530 + </dependency>
  531 +
525 532 <!-- 三方云服务相关 -->
526 533 <dependency>
527 534 <groupId>com.squareup.okio</groupId>
... ...
yudao-framework/yudao-spring-boot-starter-web/pom.xml
... ... @@ -67,6 +67,11 @@
67 67 <scope>provided</scope> <!-- 设置为 provided,主要是 GlobalExceptionHandler 使用 -->
68 68 </dependency>
69 69  
  70 + <!-- xss -->
  71 + <dependency>
  72 + <groupId>org.jsoup</groupId>
  73 + <artifactId>jsoup</artifactId>
  74 + </dependency>
70 75 </dependencies>
71 76  
72 77 </project>
... ...
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java
... ... @@ -2,15 +2,22 @@ package cn.iocoder.yudao.framework.web.config;
2 2  
3 3 import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
4 4 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
  5 +import cn.iocoder.yudao.framework.web.core.clean.JsoupXssCleaner;
  6 +import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
5 7 import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter;
6 8 import cn.iocoder.yudao.framework.web.core.filter.DemoFilter;
7 9 import cn.iocoder.yudao.framework.web.core.filter.XssFilter;
8 10 import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
9 11 import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler;
  12 +import cn.iocoder.yudao.framework.web.core.json.XssStringJsonDeserializer;
10 13 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
  14 +import com.fasterxml.jackson.databind.ObjectMapper;
11 15 import org.springframework.beans.factory.annotation.Value;
12 16 import org.springframework.boot.autoconfigure.AutoConfiguration;
  17 +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
  18 +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
13 19 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  20 +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
14 21 import org.springframework.boot.context.properties.EnableConfigurationProperties;
15 22 import org.springframework.boot.web.servlet.FilterRegistrationBean;
16 23 import org.springframework.context.annotation.Bean;
... ... @@ -48,7 +55,7 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
48 55 * 设置 API 前缀,仅仅匹配 controller 包下的
49 56 *
50 57 * @param configurer 配置
51   - * @param api API 配置
  58 + * @param api API 配置
52 59 */
53 60 private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) {
54 61 AntPathMatcher antPathMatcher = new AntPathMatcher(".");
... ... @@ -104,8 +111,9 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
104 111 * 创建 XssFilter Bean,解决 Xss 安全问题
105 112 */
106 113 @Bean
107   - public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher) {
108   - return createFilterBean(new XssFilter(properties, pathMatcher), WebFilterOrderEnum.XSS_FILTER);
  114 + @ConditionalOnBean(XssCleaner.class)
  115 + public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher, XssCleaner xssCleaner) {
  116 + return createFilterBean(new XssFilter(properties, pathMatcher, xssCleaner), WebFilterOrderEnum.XSS_FILTER);
109 117 }
110 118  
111 119 /**
... ... @@ -117,6 +125,32 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
117 125 return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER);
118 126 }
119 127  
  128 +
  129 + /**
  130 + * Xss 清理者
  131 + *
  132 + * @return XssCleaner
  133 + */
  134 + @Bean
  135 + @ConditionalOnMissingBean(XssCleaner.class)
  136 + public XssCleaner xssCleaner() {
  137 + return new JsoupXssCleaner();
  138 + }
  139 +
  140 + /**
  141 + * 注册 Jackson 的序列化器,用于处理 json 类型参数的 xss 过滤
  142 + *
  143 + * @return Jackson2ObjectMapperBuilderCustomizer
  144 + */
  145 + @Bean
  146 + @ConditionalOnMissingBean(name = "xssJacksonCustomizer")
  147 + @ConditionalOnBean(ObjectMapper.class)
  148 + @ConditionalOnProperty(value = "yudao.xss.enable", havingValue = "true")
  149 + public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssCleaner xssCleaner) {
  150 + // 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理
  151 + return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(xssCleaner));
  152 + }
  153 +
120 154 private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
121 155 FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
122 156 bean.setOrder(order);
... ...
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/clean/JsoupXssCleaner.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.web.core.clean;
  2 +
  3 +import org.jsoup.Jsoup;
  4 +import org.jsoup.nodes.Document;
  5 +import org.jsoup.safety.Safelist;
  6 +
  7 +/**
  8 + * jsonp 过滤字符串
  9 + */
  10 +public class JsoupXssCleaner implements XssCleaner {
  11 +
  12 + private final Safelist safelist;
  13 +
  14 + /**
  15 + * 用于在 src 属性使用相对路径时,强制转换为绝对路径。 为空时不处理,值应为绝对路径的前缀(包含协议部分)
  16 + */
  17 + private final String baseUri;
  18 +
  19 + /**
  20 + * 无参构造,默认使用 {@link JsoupXssCleaner#buildSafelist} 方法构建一个安全列表
  21 + */
  22 + public JsoupXssCleaner() {
  23 + this.safelist = buildSafelist();
  24 + this.baseUri = "";
  25 + }
  26 +
  27 + public JsoupXssCleaner(Safelist safelist) {
  28 + this.safelist = safelist;
  29 + this.baseUri = "";
  30 + }
  31 +
  32 + public JsoupXssCleaner(String baseUri) {
  33 + this.safelist = buildSafelist();
  34 + this.baseUri = baseUri;
  35 + }
  36 +
  37 + public JsoupXssCleaner(Safelist safelist, String baseUri) {
  38 + this.safelist = safelist;
  39 + this.baseUri = baseUri;
  40 + }
  41 +
  42 + /**
  43 + * 构建一个 Xss 清理的 Safelist 规则。
  44 + * 基于 Safelist#relaxed() 的基础上:
  45 + * 1. 扩展支持了 style 和 class 属性
  46 + * 2. a 标签额外支持了 target 属性
  47 + * 3. img 标签额外支持了 data 协议,便于支持 base64
  48 + *
  49 + * @return Safelist
  50 + */
  51 + private Safelist buildSafelist() {
  52 + // 使用 jsoup 提供的默认的
  53 + Safelist relaxedSafelist = Safelist.relaxed();
  54 + // 富文本编辑时一些样式是使用 style 来进行实现的
  55 + // 比如红色字体 style="color:red;", 所以需要给所有标签添加 style 属性
  56 + // 注意:style 属性会有注入风险 <img STYLE="background-image:url(javascript:alert('XSS'))">
  57 + relaxedSafelist.addAttributes(":all", "style", "class");
  58 + // 保留 a 标签的 target 属性
  59 + relaxedSafelist.addAttributes("a", "target");
  60 + // 支持img 为base64
  61 + relaxedSafelist.addProtocols("img", "src", "data");
  62 +
  63 + // 保留相对路径, 保留相对路径时,必须提供对应的 baseUri 属性,否则依然会被删除
  64 + // WHITELIST.preserveRelativeLinks(false);
  65 +
  66 + // 移除 a 标签和 img 标签的一些协议限制,这会导致 xss 防注入失效,如 <img src=javascript:alert("xss")>
  67 + // 虽然可以重写 WhiteList#isSafeAttribute 来处理,但是有隐患,所以暂时不支持相对路径
  68 + // WHITELIST.removeProtocols("a", "href", "ftp", "http", "https", "mailto");
  69 + // WHITELIST.removeProtocols("img", "src", "http", "https");
  70 +
  71 + return relaxedSafelist;
  72 + }
  73 +
  74 + @Override
  75 + public String clean(String html) {
  76 + return Jsoup.clean(html, baseUri, safelist, new Document.OutputSettings().prettyPrint(false));
  77 + }
  78 +
  79 +}
  80 +
... ...
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/clean/XssCleaner.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.web.core.clean;
  2 +
  3 +/**
  4 + * 对 html 文本中的有 Xss 风险的数据进行清理
  5 + */
  6 +public interface XssCleaner {
  7 +
  8 + /**
  9 + * 清理有 Xss 风险的文本
  10 + *
  11 + * @param html 原 html
  12 + * @return 清理后的 html
  13 + */
  14 + String clean(String html);
  15 +}
... ...
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssFilter.java
1 1 package cn.iocoder.yudao.framework.web.core.filter;
2 2  
3 3 import cn.iocoder.yudao.framework.web.config.XssProperties;
  4 +import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
4 5 import lombok.AllArgsConstructor;
5 6 import org.springframework.util.PathMatcher;
6 7 import org.springframework.web.filter.OncePerRequestFilter;
... ... @@ -13,7 +14,7 @@ import java.io.IOException;
13 14  
14 15 /**
15 16 * Xss 过滤器
16   - *
  17 + * <p>
17 18 * 对 Xss 不了解的胖友,可以看看 http://www.iocoder.cn/Fight/The-new-girl-asked-me-why-AJAX-requests-are-not-secure-I-did-not-answer/
18 19 *
19 20 * @author 芋道源码
... ... @@ -30,10 +31,12 @@ public class XssFilter extends OncePerRequestFilter {
30 31 */
31 32 private final PathMatcher pathMatcher;
32 33  
  34 + private final XssCleaner xssCleaner;
  35 +
33 36 @Override
34 37 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
35 38 throws IOException, ServletException {
36   - filterChain.doFilter(new XssRequestWrapper(request), response);
  39 + filterChain.doFilter(new XssRequestWrapper(request, xssCleaner), response);
37 40 }
38 41  
39 42 @Override
... ...
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssRequestWrapper.java
1 1 package cn.iocoder.yudao.framework.web.core.filter;
2 2  
3   -import cn.hutool.core.collection.CollUtil;
4   -import cn.hutool.core.io.IoUtil;
5   -import cn.hutool.core.util.ArrayUtil;
6   -import cn.hutool.core.util.ReflectUtil;
7   -import cn.hutool.core.util.StrUtil;
8   -import cn.hutool.http.HTMLFilter;
9   -import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
  3 +import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
10 4  
11   -import javax.servlet.ReadListener;
12   -import javax.servlet.ServletInputStream;
13 5 import javax.servlet.http.HttpServletRequest;
14 6 import javax.servlet.http.HttpServletRequestWrapper;
15   -import java.io.BufferedReader;
16   -import java.io.ByteArrayInputStream;
17   -import java.io.IOException;
18   -import java.io.InputStreamReader;
  7 +import java.util.LinkedHashMap;
19 8 import java.util.Map;
20 9  
21 10 /**
... ... @@ -24,113 +13,79 @@ import java.util.Map;
24 13 * @author 芋道源码
25 14 */
26 15 public class XssRequestWrapper extends HttpServletRequestWrapper {
  16 + private final XssCleaner xssCleaner;
27 17  
28   - /**
29   - * 基于线程级别的 HTMLFilter 对象,因为它线程非安全
30   - */
31   - private static final ThreadLocal<HTMLFilter> HTML_FILTER = ThreadLocal.withInitial(() -> {
32   - HTMLFilter htmlFilter = new HTMLFilter();
33   - // 反射修改 encodeQuotes 属性为 false,避免 " 被转移成 &quot; 字符
34   - ReflectUtil.setFieldValue(htmlFilter, "encodeQuotes", false);
35   - return htmlFilter;
36   - });
37   -
38   - public XssRequestWrapper(HttpServletRequest request) {
  18 + public XssRequestWrapper(HttpServletRequest request, XssCleaner xssCleaner) {
39 19 super(request);
  20 + this.xssCleaner = xssCleaner;
40 21 }
41 22  
42   - private static String filterXss(String content) {
43   - if (StrUtil.isEmpty(content)) {
44   - return content;
45   - }
46   - return HTML_FILTER.get().filter(content);
47   - }
48   -
49   - // ========== IO 流相关 ==========
50   -
  23 + // ============================ parameter ============================
51 24 @Override
52   - public BufferedReader getReader() throws IOException {
53   - return new BufferedReader(new InputStreamReader(this.getInputStream()));
  25 + public Map<String, String[]> getParameterMap() {
  26 + Map<String, String[]> map = new LinkedHashMap<>();
  27 + Map<String, String[]> parameters = super.getParameterMap();
  28 + for (Map.Entry<String, String[]> entry : parameters.entrySet()) {
  29 + String[] values = entry.getValue();
  30 + for (int i = 0; i < values.length; i++) {
  31 + values[i] = xssCleaner.clean(values[i]);
  32 + }
  33 + map.put(entry.getKey(), values);
  34 + }
  35 + return map;
54 36 }
55 37  
56 38 @Override
57   - public ServletInputStream getInputStream() throws IOException {
58   - // 如果非 json 请求,不进行 Xss 处理
59   - if (!ServletUtils.isJsonRequest(this)) {
60   - return super.getInputStream();
  39 + public String[] getParameterValues(String name) {
  40 + String[] values = super.getParameterValues(name);
  41 + if (values == null) {
  42 + return null;
61 43 }
62   -
63   - // 读取内容,并过滤
64   - String content = IoUtil.readUtf8(super.getInputStream());
65   - content = filterXss(content);
66   - final ByteArrayInputStream newInputStream = new ByteArrayInputStream(content.getBytes());
67   - // 返回 ServletInputStream
68   - return new ServletInputStream() {
69   -
70   - @Override
71   - public int read() {
72   - return newInputStream.read();
73   - }
74   -
75   - @Override
76   - public boolean isFinished() {
77   - return true;
78   - }
79   -
80   - @Override
81   - public boolean isReady() {
82   - return true;
83   - }
84   -
85   - @Override
86   - public void setReadListener(ReadListener readListener) {}
87   -
88   - };
  44 + int count = values.length;
  45 + String[] encodedValues = new String[count];
  46 + for (int i = 0; i < count; i++) {
  47 + encodedValues[i] = xssCleaner.clean(values[i]);
  48 + }
  49 + return encodedValues;
89 50 }
90 51  
91   - // ========== Param 相关 ==========
92   -
93 52 @Override
94 53 public String getParameter(String name) {
95 54 String value = super.getParameter(name);
96   - return filterXss(value);
  55 + if (value == null) {
  56 + return null;
  57 + }
  58 + return xssCleaner.clean(value);
97 59 }
98 60  
  61 + // ============================ attribute ============================
99 62 @Override
100   - public String[] getParameterValues(String name) {
101   - String[] values = super.getParameterValues(name);
102   - if (ArrayUtil.isEmpty(values)) {
103   - return values;
104   - }
105   - // 过滤处理
106   - for (int i = 0; i < values.length; i++) {
107   - values[i] = filterXss(values[i]);
  63 + public Object getAttribute(String name) {
  64 + Object value = super.getAttribute(name);
  65 + if (value instanceof String) {
  66 + xssCleaner.clean((String) value);
108 67 }
109   - return values;
  68 + return value;
110 69 }
111 70  
  71 + // ============================ header ============================
112 72 @Override
113   - public Map<String, String[]> getParameterMap() {
114   - Map<String, String[]> valueMap = super.getParameterMap();
115   - if (CollUtil.isEmpty(valueMap)) {
116   - return valueMap;
117   - }
118   - // 过滤处理
119   - for (Map.Entry<String, String[]> entry : valueMap.entrySet()) {
120   - String[] values = entry.getValue();
121   - for (int i = 0; i < values.length; i++) {
122   - values[i] = filterXss(values[i]);
123   - }
  73 + public String getHeader(String name) {
  74 + String value = super.getHeader(name);
  75 + if (value == null) {
  76 + return null;
124 77 }
125   - return valueMap;
  78 + return xssCleaner.clean(value);
126 79 }
127 80  
128   - // ========== Header 相关 ==========
129   -
  81 + // ============================ queryString ============================
130 82 @Override
131   - public String getHeader(String name) {
132   - String value = super.getHeader(name);
133   - return filterXss(value);
  83 + public String getQueryString() {
  84 + String value = super.getQueryString();
  85 + if (value == null) {
  86 + return null;
  87 + }
  88 + return xssCleaner.clean(value);
134 89 }
135 90  
136 91 }
... ...
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/json/XssStringJsonDeserializer.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.web.core.json;
  2 +
  3 +import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
  4 +import com.fasterxml.jackson.core.JsonParser;
  5 +import com.fasterxml.jackson.core.JsonToken;
  6 +import com.fasterxml.jackson.databind.DeserializationContext;
  7 +import com.fasterxml.jackson.databind.deser.std.StringDeserializer;
  8 +import lombok.AllArgsConstructor;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +
  11 +import java.io.IOException;
  12 +
  13 +/**
  14 + * XSS 过滤 jackson 反序列化器。
  15 + * 在反序列化的过程中,会对字符串进行 XSS 过滤。
  16 + *
  17 + * @author Hccake
  18 + */
  19 +@Slf4j
  20 +@AllArgsConstructor
  21 +public class XssStringJsonDeserializer extends StringDeserializer {
  22 +
  23 + private final XssCleaner xssCleaner;
  24 +
  25 + @Override
  26 + public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
  27 + if (p.hasToken(JsonToken.VALUE_STRING)) {
  28 + return xssCleaner.clean(p.getText());
  29 + }
  30 + JsonToken t = p.currentToken();
  31 + // [databind#381]
  32 + if (t == JsonToken.START_ARRAY) {
  33 + return _deserializeFromArray(p, ctxt);
  34 + }
  35 + // need to gracefully handle byte[] data, as base64
  36 + if (t == JsonToken.VALUE_EMBEDDED_OBJECT) {
  37 + Object ob = p.getEmbeddedObject();
  38 + if (ob == null) {
  39 + return null;
  40 + }
  41 + if (ob instanceof byte[]) {
  42 + return ctxt.getBase64Variant().encode((byte[]) ob, false);
  43 + }
  44 + // otherwise, try conversion using toString()...
  45 + return ob.toString();
  46 + }
  47 + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML)
  48 + if (t == JsonToken.START_OBJECT) {
  49 + return ctxt.extractScalarFromObject(p, this, _valueClass);
  50 + }
  51 +
  52 + if (t.isScalarValue()) {
  53 + String text = p.getValueAsString();
  54 + return xssCleaner.clean(text);
  55 + }
  56 + return (String) ctxt.handleUnexpectedToken(_valueClass, p);
  57 + }
  58 +}
  59 +
... ...