AntiSamy是OWASP的一个开源项目,通过对用户输入的 HTML / CSS / JavaScript 等内容进行检验和清理,确保输入符合应用规范。AntiSamy被广泛应用于Web服务对存储型和反射型XSS的防御中。
1、maven依赖
<dependency>
<groupId>org.owasp.antisamy</groupId>
<artifactId>antisamy</artifactId>
<version>1.6.2</version>
</dependency>
2、选择策略文件
AntiSamy对“恶意代码”的过滤依赖于策略文件,详细的可以查下资料哦。
在AntiSamy的jar包中,包含了几个常用的策略文件,我就以"antisamy-ebay.xml"为例。
3、XSS过滤器
package com.security.filter;
import com.security.wrapper.XssRequestWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class XssFilter implements Filter {
private FilterConfig config;
private final PathMatcher pathMatcher = new AntPathMatcher();
private static final Logger LOGGER = LoggerFactory.getLogger(XssFilter.class);
private final String[] NULL_STRING_ARRAY = new String[0];
private final String URL_SPLIT_PATTERN = "[, ;\r\n]";//逗号 空格 分号 换行
/**
* 白名单
*/
private String[] whiteListURLs = null;
/**
* 黑名单
*/
private String[] blackListURLs = null;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.config = filterConfig;
this.initConfig();
this.init();
}
public void init() throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String currentURL = httpRequest.getServletPath();
if (isWhiteURL(currentURL)) {
doFilter(httpRequest, httpResponse, chain);
return;
}
LOGGER.info(" 拦截请求,处理XSS过滤 当前 url : [{}]", currentURL);
// 拦截请求,处理XSS过滤
chain.doFilter(new XssRequestWrapper((HttpServletRequest)request), response);
return;
}
/**
* 子类覆盖
*
* @param request
* @param response
* @param chain
* @throws IOException
* @throws ServletException
*/
public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(request, response);
}
@Override
public void destroy() {
this.config = null;
}
private boolean isWhiteURL(String currentURL) {
for (String whiteURL : whiteListURLs) {
if (pathMatcher.match(whiteURL, currentURL)) {
return true;
}
}
return false;
}
private boolean isBlackURL(String currentURL) {
for (String blackURL : blackListURLs) {
if (pathMatcher.match(blackURL, currentURL)) {
return true;
}
}
return false;
}
private void initConfig() {
String whiteListURLStr = this.config.getInitParameter("whiteListURL");
whiteListURLs = strToArray(whiteListURLStr);
String blackListURLStr = this.config.getInitParameter("blackListURL");
blackListURLs = strToArray(blackListURLStr);
}
private String[] strToArray(String urlStr) {
if (urlStr == null) {
return NULL_STRING_ARRAY;
}
String[] urlArray = urlStr.split(URL_SPLIT_PATTERN);
List<String> urlList = new ArrayList<String>();
for (String url : urlArray) {
url = url.trim();
if (url.length() == 0) {
continue;
}
urlList.add(url);
}
return urlList.toArray(NULL_STRING_ARRAY);
}
public FilterConfig getConfig() {
return config;
}
}
4、自定义的XSS请求装饰器
装饰器模式加强对request的处理,基于AntiSamy进行XSS防御
package com.security.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;
/**
* @description 装饰器模式加强对request的处理,基于AntiSamy进行XSS防御
*/
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/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();
}
}
}
}
}
5、web.xml
黑白名单说明:
- 黑名单可以不用设置
- 白名单是不会被拦截的,如果某些请求不需要被拦截,那么可以放入白名单内。每条链接需要用逗号 空格 分号 换行 隔开。
<!-- XSS -->
<filter>
<filter-name>XSSFilter</filter-name>
<filter-class>com.security.filter.XssFilter</filter-class>
<init-param>
<param-name>blackListURL</param-name>
<param-value>
/blackListURL
</param-value>
</init-param>
<init-param>
<param-name>whiteListURL</param-name>
<param-value>
/whiteListURL/**
</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>XSSFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>