SpringBoot2.x 整合 AntiSamy防御XSS攻擊的簡單總結
AntiSamy是OWASP的一個開源項目,通過對用戶輸入的HTML、CSS、JavaScript等內(nèi)容進行檢驗和清理,確保輸入符合應用規(guī)范。AntiSamy被廣泛應用于Web服務對存儲型和反射型XSS的防御中。
XSS攻擊全稱為跨站腳本攻擊(Cross Site Scripting),是一種在web應用中的計算機安全漏洞,它允許用戶將惡意代碼(如script腳本)植入到Web頁面中,為了不和層疊樣式表(Cascading Style Sheets, CSS)混淆,一般縮寫為XSS。XSS分為以下兩種類型:
- 存儲型XSS:服務端對用戶輸入的惡意腳本沒有經(jīng)過驗證就存入數(shù)據(jù)庫,每次調用數(shù)據(jù)庫都會將其渲染在瀏覽器上。則可能為存儲型XSS。
- 反射型XSS:通過get或者post等方式,向服務端輸入數(shù)據(jù)。如果服務端不進行過濾,驗證或編碼,直接將用戶信息呈現(xiàn)出來,可能會造成反射型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格式,規(guī)定了AntiSamy對各個標簽、屬性的處理方法。策略文件定義的嚴格與否,決定了AntiSamy對Xss的防御效果。在AntiSamy的jar包中,已經(jīng)包含了幾個常用的策略文件:

本文使用antisamy-ebay.xml作為策略文件,該策略相對安全,適用于電商網(wǎng)站。將antisamy-ebay.xml和antisamy.xsd復制到resouces目錄下。對于策略文件的具體內(nèi)容這里不進行深入了解,只需了解下對標簽的處理規(guī)則<tag-rules>,共有remove、truncate、validate三種處理方式,其中remove為直接刪除,truncate為縮短標簽,只保留標簽和值,validate為驗證標簽屬性:

上圖截取了<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;
}
}
四、創(chuàng)建過濾器
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;
}
}
注意:在過濾器中并沒有直接對請求參數(shù)進行過濾清洗,而是在XssRequestWrapper類中進行的。XssRequestWrapper類將當前的request對象進行了包裝,在過濾器放行時會自動調用XssRequestWrapper中的方法對請求參數(shù)進行清洗。
五、創(chuàng)建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對象時會出現(xiàn)找不到路徑的錯誤
// 對路徑進行解碼以解決該問題
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 參數(shù)名
* @return 參數(shù)值
*/
@Override
public String getHeader(String name) {
String header = super.getHeader(name);
// 如果Header為空,則直接返回,否則進行清洗
return StringUtils.isBlank(header) ? header : xssClean(header);
}
/**
* 過濾請求參數(shù)
*
* @param name 參數(shù)名
* @return 參數(shù)值
*/
@Override
public String getParameter(String name) {
String parameter = super.getParameter(name);
// 如果Parameter為空,則直接返回,否則進行清洗
return StringUtils.isBlank(parameter) ? parameter : xssClean(parameter);
}
/**
* 過濾請求參數(shù)(一個參數(shù)可以有多個值)
*
* @param name 參數(shù)名
* @return 參數(shù)值數(shù)組
*/
@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清理之前的參數(shù)值:" + parameterValues[i]);
// 清洗參數(shù)
newParameterValues[i] = xssClean(parameterValues[i]);
LOGGER.info("AntiSamy清理之后的參數(shù)值:" + 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清洗數(shù)據(jù)
*
* @param value 需要清洗的數(shù)據(jù)
* @return 清洗后的數(shù)據(jù)
*/
private String xssClean(String value) {
try {
AntiSamy antiSamy = new AntiSamy();
// 使用AntiSamy清洗數(shù)據(jù)
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();
}
}
}
}
}
六、創(chuàng)建配置類
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類型數(shù)據(jù)的解析器
*
* @param builder Jackson2ObjectMapperBuilder
* @return ObjectMapper
*/
@Bean
public ObjectMapper xssObjectMapper(Jackson2ObjectMapperBuilder builder) {
// 創(chuàng)建解析器
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
// 注冊解析器
SimpleModule simpleModule = new SimpleModule("XssStringJsonSerializer");
simpleModule.addSerializer(new XssRequestWrapper.XssStringJsonSerializer());
objectMapper.registerModule(simpleModule);
return objectMapper;
}
}
七、測試
啟動項目,發(fā)送如下POST請求,請求地址為http://localhost:8080/user/save,可見表單參數(shù)中的<script>標簽內(nèi)容被成功過濾:

發(fā)送如下GET請求,請求地址為http://localhost:8080/user/get?id=1<script>alert("XSS");</script>0,可見Query參數(shù)中的<script>標簽內(nèi)容被成功過濾:

發(fā)送如下PUT請求,請求地址為http://localhost:8080/user/update,可見Json類型參數(shù)中的<script>標簽內(nèi)容被成功過濾:

代碼示例
到此這篇關于SpringBoot2.x 整合 AntiSamy防御XSS攻擊的簡單總結的文章就介紹到這了,更多相關SpringBoot2.x防御XSS攻擊內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
spring boot如何基于JWT實現(xiàn)單點登錄詳解
這篇文章主要介紹了spring boot如何基于JWT實現(xiàn)單點登錄詳解,用戶只需登錄一次就能夠在這兩個系統(tǒng)中進行操作。很明顯這就是單點登錄(Single Sign-On)達到的效果,需要的朋友可以參考下2019-06-06

