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>
<dependency>
<groupId>org.owasp.antisamy</groupId>
<artifactId>antisamy</artifactId>
<version>1.6.2</version>
</dependency>
<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;
@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.*;
@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;
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 {
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;
public class XssRequestWrapper extends HttpServletRequestWrapper {
private static final Logger LOGGER = LoggerFactory.getLogger(XssRequestWrapper.class);
private static Policy policy = null;
static {
try {
String antiSamyPath = Objects
.requireNonNull(XssRequestWrapper.class.getClassLoader().getResource("antisamy-ebay.xml")).getFile();
LOGGER.info(antiSamyPath);
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);
}
@Override
public String getHeader(String name) {
String header = super.getHeader(name);
return StringUtils.isBlank(header) ? header : xssClean(header);
}
@Override
public String getParameter(String name) {
String parameter = super.getParameter(name);
return StringUtils.isBlank(parameter) ? parameter : xssClean(parameter);
}
@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;
}
private String xssClean(String value) {
try {
AntiSamy antiSamy = new AntiSamy();
final CleanResults cleanResults = antiSamy.scan(value, policy);
value = cleanResults.getCleanHTML();
return StringEscapeUtils.unescapeHtml4(value);
} catch (ScanException | PolicyException e) {
e.printStackTrace();
}
return value;
}
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;
@Configuration
public class AntiSamyConfig {
@Bean
public FilterRegistrationBean<Filter> filterRegistrationBean() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>(new XssFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setOrder(1);
return filterRegistrationBean;
}
@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> 标签内容被成功过滤:
代码示例
|