当前位置 博文首页 > 文章内容

    SpringBoot2.x 整合 AntiSamy防御XSS攻击的简单总结

    作者:shunshunshun18 栏目:未分类 时间:2021-08-26 14:42:17

    本站于2023年9月4日。收到“大连君*****咨询有限公司”通知
    说我们IIS7站长博客,有一篇博文用了他们的图片。
    要求我们给他们一张图片6000元。要不然法院告我们

    为避免不必要的麻烦,IIS7站长博客,全站内容图片下架、并积极应诉
    博文内容全部不再显示,请需要相关资讯的站长朋友到必应搜索。谢谢!

    另祝:版权碰瓷诈骗团伙,早日弃暗投明。

    相关新闻:借版权之名、行诈骗之实,周某因犯诈骗罪被判处有期徒刑十一年六个月

    叹!百花齐放的时代,渐行渐远!



    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包中,已经包含了几个常用的策略文件:

    1

    本文使用antisamy-ebay.xml作为策略文件,该策略相对安全,适用于电商网站。将antisamy-ebay.xmlantisamy.xsd复制到resouces目录下。对于策略文件的具体内容这里不进行深入了解,只需了解下对标签的处理规则<tag-rules>,共有remove、truncate、validate三种处理方式,其中remove为直接删除,truncate为缩短标签,只保留标签和值,validate为验证标签属性:

    2

    上图截取了<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>标签内容被成功过滤:

    3

    发送如下GET请求,请求地址为0" rel="external nofollow" >http://localhost:8080/user/get?id=1<script>alert("XSS");</script>0,可见Query参数中的<script>标签内容被成功过滤:

    4

    发送如下PUT请求,请求地址为http://localhost:8080/user/update,可见Json类型参数中的<script>标签内容被成功过滤:

    5

    代码示例

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

    Gitee:https://gitee.com/RtxTitanV/springboot-learning/tree/master/springboot2.x-learning/springboot-antisamy