什么是xss
xss:跨站脚本攻击(Cross Site Scripting),因为跟样式css混淆,所以习惯缩写为xss。通过一些方法注入恶意指令代码到网页,使其加载并执行攻击者恶意的网页程序。
xss类型
1、反射型xss:通过get或者post等方式,向服务端输入数据。如果服务端不进行处理(过滤,验证,编码等),直接将信息呈现出来,可能会造成反射型xss。
2、存储型xss:服务端对注入的恶意脚本没有经过验证存入数据库,每次调用数据库都会将其渲染在浏览器上。则可能为存储型xss。
AntiSamy防御
主要思路为:对用户输入的脚本,提交的数据进行转义,编码。
AntiSamy提供了对恶意指令的过滤,各个标签、属性的处理方法。主要通过定义策略文件来达到防御的效果。
1、导入jar
<dependency>
<groupId>org.owasp.antisamy</groupId>
<artifactId>antisamy</artifactId>
<version>1.5.5</version>
</dependency>
2、定义策略文件antisamy-slashdot.xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<anti-samy-rules xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="antisamy.xsd">
<!-- 全局配置,对AntiSamy的过滤验证规则、输入、输出的格式进行控制 -->
<directives>
<directive name="omitXmlDeclaration" value="true"/>
<directive name="omitDoctypeDeclaration" value="true"/>
<directive name="maxInputSize" value="500000"/>
<directive name="useXHTML" value="true"/>
<directive name="formatOutput" value="false"/>
<directive name="preserveComments" value="true"/>
<directive name="onUnknownTag" value="encode"/>
<directive name="nofollowAnchors" value="true" />
<directive name="embedStyleSheets" value="false"/>
</directives>
<!-- 公用的正则表达式 -->
<common-regexps>
<regexp name="htmlTitle"
value="[\p{L}\p{N}\s\-_',:\[\]!\./\\\(\)&]*"/> <!-- force non-empty with a '+' at the end instead of '*' -->
<regexp name="onsiteURL" value="([\p{L}\p{N}\\/\.\?=\#&;\-_~]+|\#(\w)+)"/>
<regexp name="offsiteURL"
value="(\s)*((ht|f)tp(s?)://|mailto:)[\p{L}\p{N}]+[~\p{L}\p{N}\p{Zs}\-_\.@\#\$%&;:,\?=/\+!\(\)]*(\s)*"/>
</common-regexps>
<!-- 通用属性需要满足的输入规则 -->
<common-attributes>
<attribute name="lang"
description="The 'lang' attribute tells the browser what language the element's attribute values and content are written in">
<regexp-list>
<regexp value="[a-zA-Z]{2,20}"/>
</regexp-list>
</attribute>
<attribute name="title"
description="The 'title' attribute provides text that shows up in a 'tooltip' when a user hovers their mouse over the element">
<regexp-list>
<regexp name="htmlTitle"/>
</regexp-list>
</attribute>
<attribute name="href" onInvalid="filterTag">
<regexp-list>
<regexp name="onsiteURL"/>
<regexp name="offsiteURL"/>
</regexp-list>
</attribute>
<attribute name="align"
description="The 'align' attribute of an HTML element is a direction word, like 'left', 'right' or 'center'">
<literal-list>
<literal value="center"/>
<literal value="left"/>
<literal value="right"/>
<literal value="justify"/>
<literal value="char"/>
</literal-list>
</attribute>
</common-attributes>
<!-- 标签默认属性遵守的规则 -->
<global-tag-attributes>
<attribute name="title"/>
<attribute name="lang"/>
</global-tag-attributes>
<!-- 需要进行编码处理的标签 -->
<tags-to-encode>
<tag>g</tag>
<tag>grin</tag>
</tags-to-encode>
<!-- 标签的处理规则:
1、remove:对应标签直接删除
2、truncate:对应标签进行缩短处理,删除所有属性,只保留标签和值
3、validate:对应标签的属性进行验证,如果tag中有定义的验证规则,则执行该规则,如果没有定义,则按照<global-tag-attributes>定义的处理
-->
<tag-rules>
<!-- Tags related to JavaScript -->
<tag name="script" action="remove"/>
<tag name="noscript" action="remove"/>
<!-- Frame & related tags -->
<tag name="iframe" action="remove"/>
<tag name="frameset" action="remove"/>
<tag name="frame" action="remove"/>
<tag name="noframes" action="remove"/>
<!-- CSS related tags -->
<tag name="style" action="remove"/>
<!-- All reasonable formatting tags -->
<tag name="p" action="validate">
<attribute name="align"/>
</tag>
<tag name="div" action="validate"/>
<tag name="i" action="validate"/>
<tag name="b" action="validate"/>
<tag name="em" action="validate"/>
<tag name="blockquote" action="validate"/>
<tag name="tt" action="validate"/>
<tag name="strong" action="validate"/>
<tag name="br" action="truncate"/>
<!-- Custom Slashdot tags, though we're trimming the idea of having a possible mismatching end tag with the endtag="" attribute -->
<tag name="quote" action="validate"/>
<tag name="ecode" action="validate"/>
<!-- Anchor and anchor related tags -->
<tag name="a" action="validate">
<attribute name="href" onInvalid="filterTag"/>
<attribute name="nohref">
<literal-list>
<literal value="nohref"/>
<literal value=""/>
</literal-list>
</attribute>
<attribute name="rel">
<literal-list>
<literal value="nofollow"/>
</literal-list>
</attribute>
</tag>
<!-- List tags -->
<tag name="ul" action="validate"/>
<tag name="ol" action="validate"/>
<tag name="li" action="validate"/>
</tag-rules>
<!-- No CSS on Slashdot posts -->
<!-- css处理规则 -->
<css-rules>
</css-rules>
</anti-samy-rules>
AntiSamy的策略文件类型有如下:
**antisamy-anythinggoes.xml:**允许所有有效的html和css的输入(但能拒绝JavaScript或跟CSS相关的网络钓鱼攻击)。一般不建议使用。
**antisamy-ebay.xml:**允许任何人发布一系列富html的内容。适用于电商网站。
**antisamy-myspace.xml:**允许提交除了javascript之外的几乎所有的html和css。不建议使用。
**antisamy-slashdot.xml:**只允许提交b、u、i、a、blockquote的标签,不支持css。适用于新闻网站的评论过滤。
**antisamy-tinymce.xml:**只允许文本格式通过。
**antisamy.xml:**默认规则,允许大部分html通过。
3、定义xss过滤器
package com.yllt.common.filter.front;
import com.alibaba.fastjson.JSON;
import com.yllt.common.util.CollectionUtil;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class XssFilter implements Filter {
private static Logger log = LoggerFactory.getLogger(XssFilter.class);
/**
* 可放行的请求路径
*/
private static final String IGNORE_PATH = "ignorePath";
/**
* 可放行的参数值
*/
private static final String IGNORE_PARAM_VALUE = "ignoreParamValue";
/**
* 默认放行单点登录的登出响应(响应中包含samlp:LogoutRequest标签,直接放行)
*/
private static final String CAS_LOGOUT_RESPONSE_TAG = "samlp:LogoutRequest";
/**
* 可放行的请求路径列表
*/
private List<String> ignorePathList;
/**
* 可放行的参数值列表
*/
private List<String> ignoreParamValueList;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("XSS fiter [XSSFilter] init start ...");
String ignorePaths = filterConfig.getInitParameter(IGNORE_PATH);
String ignoreParamValues = filterConfig.getInitParameter(IGNORE_PARAM_VALUE);
if (!StringUtils.isBlank(ignorePaths)) {
String[] ignorePathArr = ignorePaths.split(",");
ignorePathList = Arrays.asList(ignorePathArr);
}
if (!StringUtils.isBlank(ignoreParamValues)) {
String[] ignoreParamValueArr = ignoreParamValues.split(",");
ignoreParamValueList = Arrays.asList(ignoreParamValueArr);
//默认放行单点登录的登出响应(响应中包含samlp:LogoutRequest标签,直接放行)
if (!ignoreParamValueList.contains(CAS_LOGOUT_RESPONSE_TAG)) {
ignoreParamValueList.add(CAS_LOGOUT_RESPONSE_TAG);
}
} else {
//默认放行单点登录的登出响应(响应中包含samlp:LogoutRequest标签,直接放行)
ignoreParamValueList = new ArrayList<String>();
ignoreParamValueList.add(CAS_LOGOUT_RESPONSE_TAG);
}
log.info("ignorePathList=" + JSON.toJSONString(ignorePathList));
log.info("ignoreParamValueList=" + JSON.toJSONString(ignoreParamValueList));
log.info("XSS fiter [XSSFilter] init end");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
log.info("XSS fiter [XSSFilter] starting");
// 判断uri是否包含项目名称
String uriPath = ((HttpServletRequest) request).getRequestURI();
if (isIgnorePath(uriPath)) {
log.info("ignore xssfilter,path[" + uriPath + "] pass through XssFilter, go ahead...");
chain.doFilter(request, response);
return;
} else {
log.info("has xssfiter path[" + uriPath + "] need XssFilter, go to XssRequestWrapper");
chain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) request, ignoreParamValueList), response);
}
log.info("XSS fiter [XSSFilter] stop");
}
@Override
public void destroy() {
log.info("XSS fiter [XSSFilter] destroy");
}
private boolean isIgnorePath(String servletPath) {
if (StringUtils.isBlank(servletPath)) {
return true;
}
if (CollectionUtil.isListNULL(ignorePathList)) {
return false;
} else {
for (String ignorePath : ignorePathList) {
if (!StringUtils.isBlank(ignorePath) && servletPath.contains(ignorePath.trim())) {
return true;
}
}
}
return false;
}
}
做一个装饰器类,来处理request的参数
package com.yllt.common.filter.front;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.List;
import java.util.Map;
import static com.yllt.common.filter.front.XssUtils.xssClean;
/**
* @author asus
*/
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
private static Logger log = LoggerFactory.getLogger(XssHttpServletRequestWrapper.class);
private List<String> ignoreParamValueList;
public XssHttpServletRequestWrapper(HttpServletRequest request, List<String> ignoreParamValueList) {
super(request);
this.ignoreParamValueList = ignoreParamValueList;
}
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> requestMap = super.getParameterMap();
for (Map.Entry<String, String[]> me : requestMap.entrySet()) {
log.info(me.getKey() + ":");
String[] values = me.getValue();
for (int i = 0; i < values.length; i++) {
log.info(values[i]);
values[i] = xssClean(values[i], this.ignoreParamValueList);
}
}
return requestMap;
}
@Override
public String[] getParameterValues(String paramString) {
String[] arrayOfString1 = super.getParameterValues(paramString);
if (arrayOfString1 == null) {
return null;
}
int i = arrayOfString1.length;
String[] arrayOfString2 = new String[i];
for (int j = 0; j < i; j++) {
arrayOfString2[j] = xssClean(arrayOfString1[j], this.ignoreParamValueList);
}
return arrayOfString2;
}
@Override
public String getParameter(String paramString) {
String str = super.getParameter(paramString);
if (str == null) {
return null;
}
return xssClean(str, this.ignoreParamValueList);
}
@Override
public String getHeader(String paramString) {
String str = super.getHeader(paramString);
if (str == null) {
return null;
}
return xssClean(str, this.ignoreParamValueList);
}
}
xss工具类,用来进行过滤处理
package com.yllt.common.filter.front;
import com.yllt.common.util.CollectionUtil;
import com.yllt.common.util.StringUtil;
import org.owasp.validator.html.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* XSS 工具类, 用于过滤特殊字符
*
* @author zuihou
* @date 2019/07/02
*/
public class XssUtils {
private static Logger log = LoggerFactory.getLogger(XssUtils.class);
private static final String ANTISAMY_SLASHDOT_XML = "antisamy-slashdot-1.4.4.xml";
private static Policy policy = null;
static {
log.info(" start read XSS configfile [" + ANTISAMY_SLASHDOT_XML + "]");
InputStream inputStream = XssUtils.class.getClassLoader().getResourceAsStream(ANTISAMY_SLASHDOT_XML);
try {
policy = Policy.getInstance(inputStream);
log.info("read XSS configfile [" + ANTISAMY_SLASHDOT_XML + "] success");
} catch (PolicyException e) {
log.error("read XSS configfile [" + ANTISAMY_SLASHDOT_XML + "] fail , reason:", e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error("close XSS configfile [" + ANTISAMY_SLASHDOT_XML + "] fail , reason:", e);
}
}
}
}
/**
* 跨站攻击语句过滤 方法
*
* @param paramValue 待过滤的参数
* @param ignoreParamValueList 忽略过滤的参数列表
* @return
*/
public static String xssClean(String paramValue, List<String> ignoreParamValueList) {
AntiSamy antiSamy = new AntiSamy();
try {
log.info("raw value before xssClean: " + paramValue);
if (isIgnoreParamValue(paramValue, ignoreParamValueList)) {
log.info("ignore the xssClean,keep the raw paramValue: " + paramValue);
return paramValue;
} else {
final CleanResults cr = antiSamy.scan(paramValue, policy);
for(String msg : cr.getErrorMessages()){
log.info(msg);
}
// cr.getErrorMessages().forEach(log::debug);
String str = cr.getCleanHTML();
/*String str = StringEscapeUtils.escapeHtml(cr.getCleanHTML());
str = str.replaceAll((antiSamy.scan(" ", policy)).getCleanHTML(), "");
str = StringEscapeUtils.unescapeHtml(str);*/
str = str.replaceAll(""", "\"");
str = str.replaceAll("&", "&");
str = str.replaceAll("'", "'");
str = str.replaceAll("'", "'");
str = str.replaceAll("<", "<");
str = str.replaceAll(">", ">");
log.info("xssfilter value after xssClean" + str);
return str;
}
} catch (ScanException e) {
log.error("scan failed armter is [" + paramValue + "]", e);
} catch (PolicyException e) {
log.error("antisamy convert failed armter is [" + paramValue + "]", e);
}
return paramValue;
}
private static boolean isIgnoreParamValue(String paramValue, List<String> ignoreParamValueList) {
if (StringUtil.isBlank(paramValue)) {
return true;
}
if (CollectionUtil.isListNULL(ignoreParamValueList)) {
return false;
} else {
for (String ignoreParamValue : ignoreParamValueList) {
if (paramValue.contains(ignoreParamValue)) {
return true;
}
}
}
return false;
}
}
4、配置过滤器
<filter>
<filter-name>xssFilter</filter-name>
<filter-class>com.yllt.common.filter.front.XssFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>xssFilter</filter-name>
<url-pattern>/front/*</url-pattern>
</filter-mapping>
验证
网页端提交如下数据:
<script>alert(666)</script>
查看xss过滤日志:
2020-10-20 14:42:39,863 INFO [com.yllt.common.filter.front.XssFilter] - <XSS fiter [XSSFilter] starting>
2020-10-20 14:42:39,864 INFO [com.yllt.common.filter.front.XssFilter] - <has xssfiter path[/search.jhtml] need XssFilter, go to XssRequestWrapper>
2020-10-20 14:42:39,866 INFO [com.yllt.common.filter.front.XssUtils] - <raw value before xssClean: <script>123>
2020-10-20 14:42:39,867 INFO [com.yllt.common.filter.front.XssUtils] - <出于安全的原因,标记script不被允许。此标记不应该影响输入的显示。>
已被过滤,同时因为配置规则script标签为remove,网页端直接删除该标签。