發(fā)現(xiàn) XSS 漏洞?別急,用這招 SpringBoot 3.3 技巧輕松搞定!
在 Web 開發(fā)中,XSS(跨站腳本攻擊)是一類常見且危險的漏洞。本文將介紹如何在 Spring Boot 3.3 項(xiàng)目中使用自定義注解和過濾器來防止 XSS 攻擊,并結(jié)合前端使用 Thymeleaf 模板引擎、JavaScript 及 Bootstrap 實(shí)現(xiàn)完整的防護(hù)方案。首先,讓我們了解一下 XSS 攻擊的類型、原理及示例。
XSS 攻擊類型及原理
XSS 攻擊可以分為以下三類:
- 存儲型 XSS(Stored XSS):攻擊者將惡意腳本存儲在目標(biāo)服務(wù)器上。例如,通過提交帶有惡意腳本的表單,服務(wù)器在后續(xù)響應(yīng)中將其返回給客戶端并執(zhí)行。
- 反射型 XSS(Reflected XSS):惡意腳本作為請求的一部分被發(fā)送到服務(wù)器,然后在響應(yīng)中返回并執(zhí)行。這種攻擊通常通過帶有惡意腳本的 URL 來實(shí)現(xiàn)。
- DOM 型 XSS(DOM-based XSS):攻擊者通過修改網(wǎng)頁的 DOM 環(huán)境(例如 JavaScript 操作 DOM)來執(zhí)行惡意腳本。這種攻擊利用的是客戶端環(huán)境而非服務(wù)器。
運(yùn)行效果:
圖片
若想獲取項(xiàng)目完整代碼以及其他文章的項(xiàng)目源碼,且在代碼編寫時遇到問題需要咨詢交流,歡迎加入下方的知識星球。
攻擊示例
存儲型 XSS 示例:
<form action="/submit" method="post">
<input type="text" name="comment" value="<script>alert('XSS');</script>">
<button type="submit">Submit</button>
</form>
反射型 XSS 示例:
http://example.com/search?query=<script>alert('XSS');</script>
DOM 型 XSS 示例:
<div id="content"></div>
<script>
var unsafeContent = '<script>alert("XSS");<\/script>';
document.getElementById('content').innerHTML = unsafeContent;
</script>
這些攻擊利用了網(wǎng)頁對用戶輸入缺乏適當(dāng)?shù)尿?yàn)證和過濾,從而使得惡意代碼得以執(zhí)行。接下來,本文將介紹如何在 SpringBoot 項(xiàng)目中實(shí)現(xiàn) XSS 防護(hù)。
項(xiàng)目配置
首先,我們創(chuàng)建一個 Spring Boot 項(xiàng)目。這里是項(xiàng)目的基本結(jié)構(gòu)和配置:
項(xiàng)目結(jié)構(gòu):
src
├── main
│ ├── java
│ │ └── com
│ │ └── icoderoad
│ │ └── xss_protection
│ │ ├── XssProtectionApplication.java
│ │ ├── annotation
│ │ │ └── XssProtection.java
│ │ ├── config
│ │ │ └── WebConfig.java
│ │ ├── controller
│ │ │ └── XssController.java
│ │ ├── filter
│ │ │ └── XssFilter.java
│ │ └── util
│ │ └── XssUtil.java
│ ├── resources
│ │ ├── templates
│ │ │ └── index.html
│ │ ├── application.yml
├── pom.xml
pom.xml 配置
<?xml versinotallow="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.icoderoad</groupId>
<artifactId>xss-protection</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>xss-protection</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.12.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml 配置
server:
port: 8080
spring:
thymeleaf:
cache: false
logging:
level:
root: INFO
xss:
enabled: true
type: annotation # 兩種處理類型 annotation 或者 filter
實(shí)現(xiàn)自定義注解
我們將定義一個自定義注解,用于標(biāo)記需要進(jìn)行 XSS 過濾保護(hù)的控制器方法參數(shù)。
創(chuàng)建注解 @XssProtection
package com.icoderoad.xss_protection.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 用于標(biāo)記需要 XSS 保護(hù)的方法參數(shù)
@Documented
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface XssProtection {
}
自定義注解處理
為了確保我們的注解生效,我們需要在控制器方法參數(shù)上正確處理 @XssProtection 注解。一個有效的方法是通過自定義參數(shù)解析器。
自定義參數(shù)解析器 XssRequestParameterResolver
package com.icoderoad.xss_protection.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import com.icoderoad.xss_protection.annotation.XssProtection;
import com.icoderoad.xss_protection.util.XssUtil;
@Component
public class XssRequestParameterResolver implements HandlerMethodArgumentResolver {
private static final Logger logger = LoggerFactory.getLogger(XssRequestParameterResolver.class);
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean hasAnnotation = parameter.hasParameterAnnotation(XssProtection.class);
logger.debug("supportsParameter: {} has annotation: {}", parameter.getParameterName(), hasAnnotation);
return hasAnnotation;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String paramName = parameter.getParameterName();
String paramValue = webRequest.getParameter(paramName);
if (paramValue != null) {
return XssUtil.sanitize(paramValue);
}
return null;
}
}
創(chuàng)建過濾器
接下來,我們實(shí)現(xiàn)一個過濾器,讀取請求的內(nèi)容,并進(jìn)行 XSS 清理。
創(chuàng)建過濾器 XssFilter
package com.icoderoad.xss_protection.filter;
import java.io.IOException;
import com.icoderoad.xss_protection.util.XssUtil;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
public class XssFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
XssHttpServletRequestWrapper xssRequestWrapper = new XssHttpServletRequestWrapper(httpServletRequest);
chain.doFilter(xssRequestWrapper, response);
}
@Override
public void destroy() {}
private static class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(String name) {
String parameter = super.getParameter(name);
return parameter == null ? null : XssUtil.sanitize(parameter);
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values != null) {
for (int i = 0; i < values.length; i++) {
values[i] = XssUtil.sanitize(values[i]);
}
}
return values;
}
}
}
配置過濾器
在 SpringBoot 配置類中注冊該過濾器或參數(shù)解析器:
配置類 WebConfig
package com.icoderoad.xss_protection.config;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.icoderoad.xss_protection.filter.XssFilter;
import jakarta.servlet.Filter;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${xss.enabled}")
private boolean xssEnabled;
@Value("${xss.type}")
private String xssType;
@Autowired
private XssRequestParameterResolver xssRequestParameterResolver;
@Bean
@ConditionalOnProperty(name = "xss.type", havingValue = "filter")
public Filter xssFilter() {
return new XssFilter();
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
if (xssEnabled && "annotation".equalsIgnoreCase(xssType)) {
resolvers.add(xssRequestParameterResolver); // 優(yōu)先級最高
}
}
@Bean
@ConditionalOnProperty(name = "xss.type", havingValue = "filter")
public FilterRegistrationBean<XssFilter> xssFilterRegistrationBean() {
FilterRegistrationBean<XssFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new XssFilter());
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
創(chuàng)建控制器
創(chuàng)建一個簡單的控制器來演示我們的 XSS 保護(hù)方案:
控制器類 XssController
package com.icoderoad.xss_protection.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.icoderoad.xss_protection.annotation.XssProtection;
@Controller
public class XssController {
@GetMapping("/")
public String index() {
return "index";
}
@PostMapping("/submit")
public String submit( @XssProtection String input, Model model) {
model.addAttribute("input", input);
return "index";
}
}
創(chuàng)建前端頁面
創(chuàng)建一個 Thymeleaf 模板頁面,用于展示和提交數(shù)據(jù)。
index.html 頁面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>XSS 防護(hù)示例</title>
<link rel="stylesheet"/>
<style>
body {
padding-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<h2 class="text-center">XSS 防護(hù)示例</h2>
<form action="/submit" method="post" class="mt-4">
<div class="form-group">
<label for="input">請輸入文本:</label>
<input type="text" class="form-control" id="input" name="input" required>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
<div class="mt-4">
<h4>提交的文本:</h4>
<p th:text="${input}"></p>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>
代碼詳細(xì)講解
- 自定義注解:注解 @XssProtection 用于標(biāo)記我們希望保護(hù)的控制器參數(shù)。此注解沒有實(shí)際功能,但在參數(shù)解析器中將使用它來判斷哪些參數(shù)需要進(jìn)行 XSS 過濾。
- 過濾器:XssFilter 過濾器會以 XssHttpServletRequestWrapper 包裝請求對象。這一包裝對象的作用是讀取請求體并進(jìn)行 XSS 清理。我們使用了 Apache Commons Text 提供的方法來轉(zhuǎn)義 HTML 字符,防止惡意腳本注入。
- 參數(shù)解析器:XssProtectionResolver 自定義參數(shù)解析器在控制器方法被調(diào)用前處理標(biāo)記有 @XssProtection 注解的參數(shù)。參數(shù)解析器使用 StringEscapeUtils.escapeHtml4 方法對參數(shù)值進(jìn)行 HTML 轉(zhuǎn)義,去除潛在的 XSS 攻擊向量。
- 前端頁面:前端頁面使用 Thymeleaf 模板引擎,結(jié)合 Bootstrap 框架來創(chuàng)建一個簡單的展示頁面。用戶提交的文本會被展示在頁面上,且變化后的內(nèi)容會通過 XssProtectionResolver 進(jìn)行 XSS 處理后再顯示。
- 配置類:在配置類中注冊自定義過濾器和參數(shù)解析器,確保它們在項(xiàng)目啟動時生效。
1. 正常文本
輸入: Hello World
期望輸出: Hello World
說明: 正常文本應(yīng)保持不變,因?yàn)樗话魏螡撛诘膼阂鈨?nèi)容。
2. 簡單的 HTML 標(biāo)簽
輸入: <b>Hello</b>
期望輸出: <b>Hello</b>
說明: 為了防止 HTML 注入,將標(biāo)簽內(nèi)容轉(zhuǎn)義為實(shí)體,確保其不會被瀏覽器解釋為實(shí)際的 HTML。
3. JavaScript 注入
輸入: <script>alert('XSS');</script>
期望輸出: <script>alert('XSS');</script>
說明: 轉(zhuǎn)義 <script> 標(biāo)簽及其內(nèi)容,防止腳本注入并在瀏覽器中執(zhí)行。
4. URL 注入
輸入: <a >Click me</a>
期望輸出: <a >Click me</a>
說明: 鏈接內(nèi)容應(yīng)當(dāng)被轉(zhuǎn)義,防止惡意鏈接注入并自動執(zhí)行。
5. 惡意屬性
輸入: <img src="x" notallow="alert('XSS')">
期望輸出: <img src="x" notallow="alert('XSS')">
說明: 移除或轉(zhuǎn)義潛在的惡意屬性,如 onerror,以防止利用屬性注入執(zhí)行惡意代碼。
總結(jié)
通過上述配置,我們創(chuàng)建了一個包含完整 XSS 防護(hù)的 Spring Boot 應(yīng)用。自定義注解、過濾器和參數(shù)解析器的結(jié)合使我們的解決方案靈活且易于擴(kuò)展。此方案不僅能有效防止 XSS 攻擊,還能保證應(yīng)用的可維護(hù)性和代碼的整潔度。