什么是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,网页端直接删除该标签。