Commit d7bec143fdcd26b42d5b2d6afb68a0084a0a2819
1 parent
bbb27df5
fix: xss 启用后编辑器上传图片错误
Showing
9 changed files
with
305 additions
and
100 deletions
yudao-dependencies/pom.xml
| @@ -41,6 +41,7 @@ | @@ -41,6 +41,7 @@ | ||
| 41 | <!-- Bpm 工作流相关 --> | 41 | <!-- Bpm 工作流相关 --> |
| 42 | <flowable.version>6.8.0</flowable.version> | 42 | <flowable.version>6.8.0</flowable.version> |
| 43 | <!-- 工具类相关 --> | 43 | <!-- 工具类相关 --> |
| 44 | + <jsoup.version>1.15.3</jsoup.version> | ||
| 44 | <lombok.version>1.18.24</lombok.version> | 45 | <lombok.version>1.18.24</lombok.version> |
| 45 | <mapstruct.version>1.5.3.Final</mapstruct.version> | 46 | <mapstruct.version>1.5.3.Final</mapstruct.version> |
| 46 | <hutool.version>5.8.11</hutool.version> | 47 | <hutool.version>5.8.11</hutool.version> |
| @@ -395,6 +396,11 @@ | @@ -395,6 +396,11 @@ | ||
| 395 | 396 | ||
| 396 | <!-- 工具类相关 --> | 397 | <!-- 工具类相关 --> |
| 397 | <dependency> | 398 | <dependency> |
| 399 | + <groupId>org.jsoup</groupId> | ||
| 400 | + <artifactId>jsoup</artifactId> | ||
| 401 | + <version>${jsoup.version}</version> | ||
| 402 | + </dependency> | ||
| 403 | + <dependency> | ||
| 398 | <groupId>cn.iocoder.boot</groupId> | 404 | <groupId>cn.iocoder.boot</groupId> |
| 399 | <artifactId>yudao-common</artifactId> | 405 | <artifactId>yudao-common</artifactId> |
| 400 | <version>${revision}</version> | 406 | <version>${revision}</version> |
yudao-framework/yudao-common/pom.xml
| @@ -133,6 +133,10 @@ | @@ -133,6 +133,10 @@ | ||
| 133 | <artifactId>transmittable-thread-local</artifactId> | 133 | <artifactId>transmittable-thread-local</artifactId> |
| 134 | </dependency> | 134 | </dependency> |
| 135 | 135 | ||
| 136 | + <dependency> | ||
| 137 | + <groupId>org.jsoup</groupId> | ||
| 138 | + <artifactId>jsoup</artifactId> | ||
| 139 | + </dependency> | ||
| 136 | </dependencies> | 140 | </dependencies> |
| 137 | 141 | ||
| 138 | </project> | 142 | </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,15 +2,22 @@ package cn.iocoder.yudao.framework.web.config; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService; | 3 | import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService; |
| 4 | import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; | 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 | import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter; | 7 | import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter; |
| 6 | import cn.iocoder.yudao.framework.web.core.filter.DemoFilter; | 8 | import cn.iocoder.yudao.framework.web.core.filter.DemoFilter; |
| 7 | import cn.iocoder.yudao.framework.web.core.filter.XssFilter; | 9 | import cn.iocoder.yudao.framework.web.core.filter.XssFilter; |
| 8 | import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; | 10 | import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; |
| 9 | import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler; | 11 | import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler; |
| 12 | +import cn.iocoder.yudao.framework.web.core.json.XssStringJsonDeserializer; | ||
| 10 | import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; | 13 | import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; |
| 14 | +import com.fasterxml.jackson.databind.ObjectMapper; | ||
| 11 | import org.springframework.beans.factory.annotation.Value; | 15 | import org.springframework.beans.factory.annotation.Value; |
| 12 | import org.springframework.boot.autoconfigure.AutoConfiguration; | 16 | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| 17 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; | ||
| 18 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; | ||
| 13 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | 19 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| 20 | +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; | ||
| 14 | import org.springframework.boot.context.properties.EnableConfigurationProperties; | 21 | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| 15 | import org.springframework.boot.web.servlet.FilterRegistrationBean; | 22 | import org.springframework.boot.web.servlet.FilterRegistrationBean; |
| 16 | import org.springframework.context.annotation.Bean; | 23 | import org.springframework.context.annotation.Bean; |
| @@ -48,7 +55,7 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer { | @@ -48,7 +55,7 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer { | ||
| 48 | * 设置 API 前缀,仅仅匹配 controller 包下的 | 55 | * 设置 API 前缀,仅仅匹配 controller 包下的 |
| 49 | * | 56 | * |
| 50 | * @param configurer 配置 | 57 | * @param configurer 配置 |
| 51 | - * @param api API 配置 | 58 | + * @param api API 配置 |
| 52 | */ | 59 | */ |
| 53 | private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) { | 60 | private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) { |
| 54 | AntPathMatcher antPathMatcher = new AntPathMatcher("."); | 61 | AntPathMatcher antPathMatcher = new AntPathMatcher("."); |
| @@ -104,8 +111,9 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer { | @@ -104,8 +111,9 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer { | ||
| 104 | * 创建 XssFilter Bean,解决 Xss 安全问题 | 111 | * 创建 XssFilter Bean,解决 Xss 安全问题 |
| 105 | */ | 112 | */ |
| 106 | @Bean | 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,6 +125,32 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer { | ||
| 117 | return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER); | 125 | return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER); |
| 118 | } | 126 | } |
| 119 | 127 | ||
| 128 | + | ||
| 129 | + /** | ||
| 130 | + * Xss 清理者 | ||
| 131 | + * | ||
| 132 | + * @return XssCleaner | ||
| 133 | + */ | ||
| 134 | + @ConditionalOnMissingBean(XssCleaner.class) | ||
| 135 | + @Bean | ||
| 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 | + public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssCleaner xssCleaner, XssProperties xssProperties) { | ||
| 149 | + // 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理 | ||
| 150 | + return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(xssCleaner, xssProperties)); | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | + | ||
| 120 | private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) { | 154 | private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) { |
| 121 | FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter); | 155 | FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter); |
| 122 | bean.setOrder(order); | 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 | +public class JsoupXssCleaner implements XssCleaner { | ||
| 8 | + | ||
| 9 | + private final Safelist safelist; | ||
| 10 | + | ||
| 11 | + /** | ||
| 12 | + * 用于在 src 属性使用相对路径时,强制转换为绝对路径。 为空时不处理,值应为绝对路径的前缀(包含协议部分) | ||
| 13 | + */ | ||
| 14 | + private final String baseUri; | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 无参构造,默认使用 {@link JsoupXssCleaner#buildSafelist} 方法构建一个安全列表 | ||
| 18 | + */ | ||
| 19 | + public JsoupXssCleaner() { | ||
| 20 | + this.safelist = buildSafelist(); | ||
| 21 | + this.baseUri = ""; | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + public JsoupXssCleaner(Safelist safelist) { | ||
| 25 | + this.safelist = safelist; | ||
| 26 | + this.baseUri = ""; | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + public JsoupXssCleaner(String baseUri) { | ||
| 30 | + this.safelist = buildSafelist(); | ||
| 31 | + this.baseUri = baseUri; | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + public JsoupXssCleaner(Safelist safelist, String baseUri) { | ||
| 35 | + this.safelist = safelist; | ||
| 36 | + this.baseUri = baseUri; | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + /** | ||
| 40 | + * <p> | ||
| 41 | + * 构建一个 Xss 清理的 Safelist 规则。 | ||
| 42 | + * </p> | ||
| 43 | + * | ||
| 44 | + * <ul> | ||
| 45 | + * 基于 Safelist#relaxed() 的基础上: | ||
| 46 | + * <li>扩展支持了 style 和 class 属性</li> | ||
| 47 | + * <li>a 标签额外支持了 target 属性</li> | ||
| 48 | + * <li>img 标签额外支持了 data 协议,便于支持 base64</li> | ||
| 49 | + * </ul> | ||
| 50 | + * @return Safelist | ||
| 51 | + */ | ||
| 52 | + protected Safelist buildSafelist() { | ||
| 53 | + // 使用 jsoup 提供的默认的 | ||
| 54 | + Safelist relaxedSafelist = Safelist.relaxed(); | ||
| 55 | + // 富文本编辑时一些样式是使用 style 来进行实现的 | ||
| 56 | + // 比如红色字体 style="color:red;", 所以需要给所有标签添加 style 属性 | ||
| 57 | + // 注意:style 属性会有注入风险 <img STYLE="background-image:url(javascript:alert('XSS'))"> | ||
| 58 | + relaxedSafelist.addAttributes(":all", "style", "class"); | ||
| 59 | + // 保留 a 标签的 target 属性 | ||
| 60 | + relaxedSafelist.addAttributes("a", "target"); | ||
| 61 | + // 支持img 为base64 | ||
| 62 | + relaxedSafelist.addProtocols("img", "src", "data"); | ||
| 63 | + | ||
| 64 | + // 保留相对路径, 保留相对路径时,必须提供对应的 baseUri 属性,否则依然会被删除 | ||
| 65 | + // WHITELIST.preserveRelativeLinks(false); | ||
| 66 | + | ||
| 67 | + // 移除 a 标签和 img 标签的一些协议限制,这会导致 xss 防注入失效,如 <img src=javascript:alert("xss")> | ||
| 68 | + // 虽然可以重写 WhiteList#isSafeAttribute 来处理,但是有隐患,所以暂时不支持相对路径 | ||
| 69 | + // WHITELIST.removeProtocols("a", "href", "ftp", "http", "https", "mailto"); | ||
| 70 | + // WHITELIST.removeProtocols("img", "src", "http", "https"); | ||
| 71 | + | ||
| 72 | + return relaxedSafelist; | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + @Override | ||
| 76 | + public String clean(String html) { | ||
| 77 | + return Jsoup.clean(html, baseUri, safelist, new Document.OutputSettings().prettyPrint(false)); | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | +} | ||
| 81 | + |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/clean/XssCleaner.java
0 → 100644
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssFilter.java
| 1 | package cn.iocoder.yudao.framework.web.core.filter; | 1 | package cn.iocoder.yudao.framework.web.core.filter; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.web.config.XssProperties; | 3 | import cn.iocoder.yudao.framework.web.config.XssProperties; |
| 4 | +import cn.iocoder.yudao.framework.web.core.clean.XssCleaner; | ||
| 4 | import lombok.AllArgsConstructor; | 5 | import lombok.AllArgsConstructor; |
| 5 | import org.springframework.util.PathMatcher; | 6 | import org.springframework.util.PathMatcher; |
| 6 | import org.springframework.web.filter.OncePerRequestFilter; | 7 | import org.springframework.web.filter.OncePerRequestFilter; |
| @@ -13,7 +14,7 @@ import java.io.IOException; | @@ -13,7 +14,7 @@ import java.io.IOException; | ||
| 13 | 14 | ||
| 14 | /** | 15 | /** |
| 15 | * Xss 过滤器 | 16 | * Xss 过滤器 |
| 16 | - * | 17 | + * <p> |
| 17 | * 对 Xss 不了解的胖友,可以看看 http://www.iocoder.cn/Fight/The-new-girl-asked-me-why-AJAX-requests-are-not-secure-I-did-not-answer/ | 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 | * @author 芋道源码 | 20 | * @author 芋道源码 |
| @@ -30,10 +31,12 @@ public class XssFilter extends OncePerRequestFilter { | @@ -30,10 +31,12 @@ public class XssFilter extends OncePerRequestFilter { | ||
| 30 | */ | 31 | */ |
| 31 | private final PathMatcher pathMatcher; | 32 | private final PathMatcher pathMatcher; |
| 32 | 33 | ||
| 34 | + private final XssCleaner xssCleaner; | ||
| 35 | + | ||
| 33 | @Override | 36 | @Override |
| 34 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | 37 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) |
| 35 | throws IOException, ServletException { | 38 | throws IOException, ServletException { |
| 36 | - filterChain.doFilter(new XssRequestWrapper(request), response); | 39 | + filterChain.doFilter(new XssRequestWrapper(request, xssCleaner), response); |
| 37 | } | 40 | } |
| 38 | 41 | ||
| 39 | @Override | 42 | @Override |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssRequestWrapper.java
| 1 | package cn.iocoder.yudao.framework.web.core.filter; | 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 | import javax.servlet.http.HttpServletRequest; | 5 | import javax.servlet.http.HttpServletRequest; |
| 14 | import javax.servlet.http.HttpServletRequestWrapper; | 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 | import java.util.Map; | 8 | import java.util.Map; |
| 20 | 9 | ||
| 21 | /** | 10 | /** |
| @@ -24,113 +13,75 @@ import java.util.Map; | @@ -24,113 +13,75 @@ import java.util.Map; | ||
| 24 | * @author 芋道源码 | 13 | * @author 芋道源码 |
| 25 | */ | 14 | */ |
| 26 | public class XssRequestWrapper extends HttpServletRequestWrapper { | 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,避免 " 被转移成 " 字符 | ||
| 34 | - ReflectUtil.setFieldValue(htmlFilter, "encodeQuotes", false); | ||
| 35 | - return htmlFilter; | ||
| 36 | - }); | ||
| 37 | - | ||
| 38 | - public XssRequestWrapper(HttpServletRequest request) { | 18 | + public XssRequestWrapper(HttpServletRequest request, XssCleaner xssCleaner) { |
| 39 | super(request); | 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 | - | ||
| 51 | @Override | 23 | @Override |
| 52 | - public BufferedReader getReader() throws IOException { | ||
| 53 | - return new BufferedReader(new InputStreamReader(this.getInputStream())); | 24 | + public Map<String, String[]> getParameterMap() { |
| 25 | + Map<String, String[]> map = new LinkedHashMap<>(); | ||
| 26 | + Map<String, String[]> parameters = super.getParameterMap(); | ||
| 27 | + for (Map.Entry<String, String[]> entry : parameters.entrySet()) { | ||
| 28 | + String[] values = entry.getValue(); | ||
| 29 | + for (int i = 0; i < values.length; i++) { | ||
| 30 | + values[i] = xssCleaner.clean(values[i]); | ||
| 31 | + } | ||
| 32 | + map.put(entry.getKey(), values); | ||
| 33 | + } | ||
| 34 | + return map; | ||
| 54 | } | 35 | } |
| 55 | 36 | ||
| 56 | @Override | 37 | @Override |
| 57 | - public ServletInputStream getInputStream() throws IOException { | ||
| 58 | - // 如果非 json 请求,不进行 Xss 处理 | ||
| 59 | - if (!ServletUtils.isJsonRequest(this)) { | ||
| 60 | - return super.getInputStream(); | 38 | + public String[] getParameterValues(String name) { |
| 39 | + String[] values = super.getParameterValues(name); | ||
| 40 | + if (values == null) { | ||
| 41 | + return null; | ||
| 61 | } | 42 | } |
| 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 | - }; | 43 | + int count = values.length; |
| 44 | + String[] encodedValues = new String[count]; | ||
| 45 | + for (int i = 0; i < count; i++) { | ||
| 46 | + encodedValues[i] = xssCleaner.clean(values[i]); | ||
| 47 | + } | ||
| 48 | + return encodedValues; | ||
| 89 | } | 49 | } |
| 90 | 50 | ||
| 91 | - // ========== Param 相关 ========== | ||
| 92 | - | ||
| 93 | @Override | 51 | @Override |
| 94 | public String getParameter(String name) { | 52 | public String getParameter(String name) { |
| 95 | String value = super.getParameter(name); | 53 | String value = super.getParameter(name); |
| 96 | - return filterXss(value); | 54 | + if (value == null) { |
| 55 | + return null; | ||
| 56 | + } | ||
| 57 | + return xssCleaner.clean(value); | ||
| 97 | } | 58 | } |
| 98 | 59 | ||
| 99 | @Override | 60 | @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]); | 61 | + public Object getAttribute(String name) { |
| 62 | + Object value = super.getAttribute(name); | ||
| 63 | + if (value instanceof String) { | ||
| 64 | + xssCleaner.clean((String) value); | ||
| 108 | } | 65 | } |
| 109 | - return values; | 66 | + return value; |
| 110 | } | 67 | } |
| 111 | 68 | ||
| 112 | @Override | 69 | @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 | - } | 70 | + public String getHeader(String name) { |
| 71 | + String value = super.getHeader(name); | ||
| 72 | + if (value == null) { | ||
| 73 | + return null; | ||
| 124 | } | 74 | } |
| 125 | - return valueMap; | 75 | + return xssCleaner.clean(value); |
| 126 | } | 76 | } |
| 127 | 77 | ||
| 128 | - // ========== Header 相关 ========== | ||
| 129 | - | ||
| 130 | @Override | 78 | @Override |
| 131 | - public String getHeader(String name) { | ||
| 132 | - String value = super.getHeader(name); | ||
| 133 | - return filterXss(value); | 79 | + public String getQueryString() { |
| 80 | + String value = super.getQueryString(); | ||
| 81 | + if (value == null) { | ||
| 82 | + return null; | ||
| 83 | + } | ||
| 84 | + return xssCleaner.clean(value); | ||
| 134 | } | 85 | } |
| 135 | 86 | ||
| 136 | } | 87 | } |
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.config.XssProperties; | ||
| 4 | +import cn.iocoder.yudao.framework.web.core.clean.XssCleaner; | ||
| 5 | +import com.fasterxml.jackson.core.JsonParser; | ||
| 6 | +import com.fasterxml.jackson.core.JsonToken; | ||
| 7 | +import com.fasterxml.jackson.databind.DeserializationContext; | ||
| 8 | +import com.fasterxml.jackson.databind.deser.std.StringDeserializer; | ||
| 9 | +import lombok.AllArgsConstructor; | ||
| 10 | +import lombok.extern.slf4j.Slf4j; | ||
| 11 | + | ||
| 12 | +import java.io.IOException; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * XSS过滤 jackson 反序列化器 | ||
| 16 | + * | ||
| 17 | + * 参考 ballcat 实现 | ||
| 18 | + */ | ||
| 19 | +@Slf4j | ||
| 20 | +@AllArgsConstructor | ||
| 21 | +public class XssStringJsonDeserializer extends StringDeserializer { | ||
| 22 | + | ||
| 23 | + private final XssCleaner xssCleaner; | ||
| 24 | + private final XssProperties xssProperties; | ||
| 25 | + | ||
| 26 | + @Override | ||
| 27 | + public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { | ||
| 28 | + if (p.hasToken(JsonToken.VALUE_STRING)) { | ||
| 29 | + return getCleanText(p.getText()); | ||
| 30 | + } | ||
| 31 | + JsonToken t = p.currentToken(); | ||
| 32 | + // [databind#381] | ||
| 33 | + if (t == JsonToken.START_ARRAY) { | ||
| 34 | + return _deserializeFromArray(p, ctxt); | ||
| 35 | + } | ||
| 36 | + // need to gracefully handle byte[] data, as base64 | ||
| 37 | + if (t == JsonToken.VALUE_EMBEDDED_OBJECT) { | ||
| 38 | + Object ob = p.getEmbeddedObject(); | ||
| 39 | + if (ob == null) { | ||
| 40 | + return null; | ||
| 41 | + } | ||
| 42 | + if (ob instanceof byte[]) { | ||
| 43 | + return ctxt.getBase64Variant().encode((byte[]) ob, false); | ||
| 44 | + } | ||
| 45 | + // otherwise, try conversion using toString()... | ||
| 46 | + return ob.toString(); | ||
| 47 | + } | ||
| 48 | + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) | ||
| 49 | + if (t == JsonToken.START_OBJECT) { | ||
| 50 | + return ctxt.extractScalarFromObject(p, this, _valueClass); | ||
| 51 | + } | ||
| 52 | + // allow coercions for other scalar types | ||
| 53 | + // 17-Jan-2018, tatu: Related to [databind#1853] avoid FIELD_NAME by ensuring it's | ||
| 54 | + // "real" scalar | ||
| 55 | + if (t.isScalarValue()) { | ||
| 56 | + String text = p.getValueAsString(); | ||
| 57 | + return getCleanText(text); | ||
| 58 | + } | ||
| 59 | + return (String) ctxt.handleUnexpectedToken(_valueClass, p); | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + private String getCleanText(String text) { | ||
| 63 | + if (text == null) { | ||
| 64 | + return null; | ||
| 65 | + } | ||
| 66 | + return xssProperties.isEnable() ? xssCleaner.clean(text) : text; | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | +} | ||
| 70 | + |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/json/XssStringJsonSerializer.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.web.core.json; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.web.config.XssProperties; | ||
| 4 | +import cn.iocoder.yudao.framework.web.core.clean.XssCleaner; | ||
| 5 | +import com.fasterxml.jackson.core.JsonGenerator; | ||
| 6 | +import com.fasterxml.jackson.databind.JsonSerializer; | ||
| 7 | +import com.fasterxml.jackson.databind.SerializerProvider; | ||
| 8 | +import lombok.AllArgsConstructor; | ||
| 9 | + | ||
| 10 | +import java.io.IOException; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * XSS过滤 jackson 序列化器 | ||
| 14 | + * | ||
| 15 | + * 参考 ballcat 实现 | ||
| 16 | + */ | ||
| 17 | +@AllArgsConstructor | ||
| 18 | +public class XssStringJsonSerializer extends JsonSerializer<String> { | ||
| 19 | + | ||
| 20 | + private final XssCleaner xssCleaner; | ||
| 21 | + private final XssProperties xssProperties; | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + @Override | ||
| 25 | + public Class<String> handledType() { | ||
| 26 | + return String.class; | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + @Override | ||
| 30 | + public void serialize(String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) | ||
| 31 | + throws IOException { | ||
| 32 | + if (value != null) { | ||
| 33 | + // 开启 Xss 才进行处理 | ||
| 34 | + if (xssProperties.isEnable()) { | ||
| 35 | + value = xssCleaner.clean(value); | ||
| 36 | + } | ||
| 37 | + jsonGenerator.writeString(value); | ||
| 38 | + } | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | +} | ||
| 42 | + |