irpas技术客

SpringBoot2.x 集成 AntiSamy 防御XSS攻击_RtxTitanV

未知 4864

AntiSamy是OWASP的一个开源项目,通过对用户输入的HTML、CSS、JavaScript等内容进行检验和清理,确保输入符合应用规范。AntiSamy被广泛应用于Web服务对存储型和反射型XSS的防御中。

XSS攻击全称为跨站脚本攻击(Cross Site Scripting),是一种在web应用中的计算机安全漏洞,它允许用户将恶意代码(如script脚本)植入到Web页面中,为了不和层叠样式表(Cascading Style Sheets, CSS)混淆,一般缩写为XSS。XSS分为以下两种类型:

存储型XSS:服务端对用户输入的恶意脚本没有经过验证就存入数据库,每次调用数据库都会将其渲染在浏览器上。则可能为存储型XSS。反射型XSS:通过get或者post等方式,向服务端输入数据。如果服务端不进行过滤,验证或编码,直接将用户信息呈现出来,可能会造成反射型XSS。

本文主要对SpringBoot2.x集成AntiSamy防御XSS攻击进行简单总结,其中SpringBoot使用的2.4.5版本。

一、引入依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- AntiSamy依赖 --> <dependency> <groupId>org.owasp.antisamy</groupId> <artifactId>antisamy</artifactId> <version>1.6.2</version> </dependency> <!-- lombok插件 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.8</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> <version>1.9</version> </dependency> 二、策略文件

Antisamy对恶意代码的过滤依赖于策略文件,策略文件为xml格式,规定了AntiSamy对各个标签、属性的处理方法。策略文件定义的严格与否,决定了AntiSamy对Xss的防御效果。在AntiSamy的jar包中,已经包含了几个常用的策略文件: 本文使用antisamy-ebay.xml作为策略文件,该策略相对安全,适用于电商网站。将antisamy-ebay.xml和antisamy.xsd复制到resouces目录下。对于策略文件的具体内容这里不进行深入了解,只需了解下对标签的处理规则<tag-rules>,共有remove、truncate、validate三种处理方式,其中remove为直接删除,truncate为缩短标签,只保留标签和值,validate为验证标签属性: 上图截取了<tag-rules>的一部分,可知对script标签的处理策略是remove。

三、实体类和Controller

用户实体类:

package com.rtxtitanv.model; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * @author rtxtitanv * @version 1.0.0 * @name com.rtxtitanv.model.User * @description 用户实体类 * @date 2021/8/23 14:54 */ @AllArgsConstructor @NoArgsConstructor @Data public class User { private Long id; private String username; private String password; }

Controller:

package com.rtxtitanv.controller; import com.rtxtitanv.model.User; import org.springframework.web.bind.annotation.*; /** * @author rtxtitanv * @version 1.0.0 * @name com.rtxtitanv.controller.UserController * @description UserController * @date 2021/8/23 14:54 */ @RequestMapping("/user") @RestController public class UserController { @PostMapping("/save") public User saveUser(User user) { return user; } @GetMapping("/get") public User getUserById(@RequestParam(value = "id") Long id) { return new User(id, "ZhaoYun", "123456"); } @PutMapping("/update") public User updateUser(@RequestBody User user) { return user; } } 四、创建过滤器 package com.rtxtitanv.filter; import com.rtxtitanv.wrapper.XssRequestWrapper; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * @author rtxtitanv * @version 1.0.0 * @name com.rtxtitanv.filter.XssFilter * @description XSS过滤器 * @date 2021/8/23 15:01 */ public class XssFilter implements Filter { private FilterConfig filterConfig; @Override public void init(FilterConfig filterConfig) throws ServletException { this.filterConfig = filterConfig; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 拦截请求,处理XSS过滤 chain.doFilter(new XssRequestWrapper((HttpServletRequest)request), response); } @Override public void destroy() { this.filterConfig = null; } }

注意:在过滤器中并没有直接对请求参数进行过滤清洗,而是在XssRequestWrapper类中进行的。XssRequestWrapper类将当前的request对象进行了包装,在过滤器放行时会自动调用XssRequestWrapper中的方法对请求参数进行清洗。

五、创建XssRequestWrapper类 package com.rtxtitanv.wrapper; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringEscapeUtils; import org.owasp.validator.html.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.Map; import java.util.Objects; /** * @author rtxtitanv * @version 1.0.0 * @name com.rtxtitanv.wrapper.XssRequestWrapper * @description 装饰器模式加强对request的处理,基于AntiSamy进行XSS防御 * @date 2021/8/23 15:01 */ public class XssRequestWrapper extends HttpServletRequestWrapper { private static final Logger LOGGER = LoggerFactory.getLogger(XssRequestWrapper.class); private static Policy policy = null; static { try { // 获取策略文件路径,策略文件需要放到项目的classpath下 String antiSamyPath = Objects .requireNonNull(XssRequestWrapper.class.getClassLoader().getResource("antisamy-ebay.xml")).getFile(); LOGGER.info(antiSamyPath); // 获取的文件路径中有空格时,空格会被替换为%20,在new一个File对象时会出现找不到路径的错误 // 对路径进行解码以解决该问题 antiSamyPath = URLDecoder.decode(antiSamyPath, "utf-8"); LOGGER.info(antiSamyPath); // 指定策略文件 policy = Policy.getInstance(antiSamyPath); } catch (UnsupportedEncodingException | PolicyException e) { e.printStackTrace(); } } public XssRequestWrapper(HttpServletRequest request) { super(request); } /** * 过滤请求头 * * @param name 参数名 * @return 参数值 */ @Override public String getHeader(String name) { String header = super.getHeader(name); // 如果Header为空,则直接返回,否则进行清洗 return StringUtils.isBlank(header) ? header : xssClean(header); } /** * 过滤请求参数 * * @param name 参数名 * @return 参数值 */ @Override public String getParameter(String name) { String parameter = super.getParameter(name); // 如果Parameter为空,则直接返回,否则进行清洗 return StringUtils.isBlank(parameter) ? parameter : xssClean(parameter); } /** * 过滤请求参数(一个参数可以有多个值) * * @param name 参数名 * @return 参数值数组 */ @Override public String[] getParameterValues(String name) { String[] parameterValues = super.getParameterValues(name); if (parameterValues != null) { int length = parameterValues.length; String[] newParameterValues = new String[length]; for (int i = 0; i < length; i++) { LOGGER.info("AntiSamy清理之前的参数值:" + parameterValues[i]); // 清洗参数 newParameterValues[i] = xssClean(parameterValues[i]); LOGGER.info("AntiSamy清理之后的参数值:" + newParameterValues[i]); } return newParameterValues; } return super.getParameterValues(name); } @Override public Map<String, String[]> getParameterMap() { Map<String, String[]> requestMap = super.getParameterMap(); requestMap.forEach((key, value) -> { for (int i = 0; i < value.length; i++) { LOGGER.info(value[i]); value[i] = xssClean(value[i]); LOGGER.info(value[i]); } }); return requestMap; } /** * 使用AntiSamy清洗数据 * * @param value 需要清洗的数据 * @return 清洗后的数据 */ private String xssClean(String value) { try { AntiSamy antiSamy = new AntiSamy(); // 使用AntiSamy清洗数据 final CleanResults cleanResults = antiSamy.scan(value, policy); // 获得安全的HTML输出 value = cleanResults.getCleanHTML(); // 对转义的HTML特殊字符(<、>、"等)进行反转义,因为AntiSamy调用scan方法时会将特殊字符转义 return StringEscapeUtils.unescapeHtml4(value); } catch (ScanException | PolicyException e) { e.printStackTrace(); } return value; } /** * 通过修改Json序列化的方式来完成Json格式的XSS过滤 */ public static class XssStringJsonSerializer extends JsonSerializer<String> { @Override public Class<String> handledType() { return String.class; } @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (!StringUtils.isBlank(value)) { try { AntiSamy antiSamy = new AntiSamy(); final CleanResults cleanResults = antiSamy.scan(value, XssRequestWrapper.policy); gen.writeString(StringEscapeUtils.unescapeHtml4(cleanResults.getCleanHTML())); } catch (ScanException | PolicyException e) { e.printStackTrace(); } } } } } 六、创建配置类 package com.rtxtitanv.config; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.rtxtitanv.filter.XssFilter; import com.rtxtitanv.wrapper.XssRequestWrapper; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import javax.servlet.Filter; /** * @author rtxtitanv * @version 1.0.0 * @name com.rtxtitanv.config.AntiSamyConfig * @description AntiSamy配置类 * @date 2021/8/23 15:05 */ @Configuration public class AntiSamyConfig { /** * 配置XSS过滤器 * * @return FilterRegistrationBean */ @Bean public FilterRegistrationBean<Filter> filterRegistrationBean() { FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>(new XssFilter()); filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.setOrder(1); return filterRegistrationBean; } /** * 用于过滤Json类型数据的解析器 * * @param builder Jackson2ObjectMapperBuilder * @return ObjectMapper */ @Bean public ObjectMapper xssObjectMapper(Jackson2ObjectMapperBuilder builder) { // 创建解析器 ObjectMapper objectMapper = builder.createXmlMapper(false).build(); // 注册解析器 SimpleModule simpleModule = new SimpleModule("XssStringJsonSerializer"); simpleModule.addSerializer(new XssRequestWrapper.XssStringJsonSerializer()); objectMapper.registerModule(simpleModule); return objectMapper; } } 七、测试

启动项目,发送如下POST请求,请求地址为http://localhost:8080/user/save,可见表单参数中的<script>标签内容被成功过滤: 发送如下GET请求,请求地址为http://localhost:8080/user/get?id=1<script>alert("XSS");</script>0,可见Query参数中的<script>标签内容被成功过滤: 发送如下PUT请求,请求地址为http://localhost:8080/user/update,可见Json类型参数中的<script>标签内容被成功过滤:

代码示例

Github:https://github.com/RtxTitanV/springboot-learning/tree/master/springboot2.x-learning/springboot-antisamyGitee:https://gitee.com/RtxTitanV/springboot-learning/tree/master/springboot2.x-learning/springboot-antisamy


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #SpringBoot2X #集成 #AntiSamy #防御XSS攻击 #Site