SpringBoot2.x 整合 AntiSamy防禦XSS攻擊的簡單總結

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請求,請求地址為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

到此這篇關於SpringBoot2.x 整合 AntiSamy防禦XSS攻擊的簡單總結的文章就介紹到這瞭,更多相關SpringBoot2.x防禦XSS攻擊內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: