在 Web 开发中,XSS(跨站脚本攻击)是一类常见且危险的漏洞。本文将介绍如何在 Spring Boot 3.3 项目中使用自定义注解和过滤器来防止 XSS 攻击,并结合前端使用 Thymeleaf 模板引擎、JavaScript 及 Bootstrap 实现完整的防护方案。首先,让我们了解一下 XSS 攻击的类型、原理及示例。
XSS 攻击类型及原理
XSS 攻击可以分为以下三类:
- 存储型 XSS(Stored XSS):攻击者将恶意脚本存储在目标服务器上。例如,通过提交带有恶意脚本的表单,服务器在后续响应中将其返回给客户端并执行。
- 反射型 XSS(Reflected XSS):恶意脚本作为请求的一部分被发送到服务器,然后在响应中返回并执行。这种攻击通常通过带有恶意脚本的 URL 来实现。
- DOM 型 XSS(DOM-based XSS):攻击者通过修改网页的 DOM 环境(例如 JavaScript 操作 DOM)来执行恶意脚本。这种攻击利用的是客户端环境而非服务器。
运行效果:
图片
若想获取项目完整代码以及其他文章的项目源码,且在代码编写时遇到问题需要咨询交流,欢迎加入下方的知识星球。
攻击示例
存储型 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>
这些攻击利用了网页对用户输入缺乏适当的验证和过滤,从而使得恶意代码得以执行。接下来,本文将介绍如何在 SpringBoot 项目中实现 XSS 防护。
项目配置
首先,我们创建一个 Spring Boot 项目。这里是项目的基本结构和配置:
项目结构:
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
实现自定义注解
我们将定义一个自定义注解,用于标记需要进行 XSS 过滤保护的控制器方法参数。
创建注解 @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;
// 用于标记需要 XSS 保护的方法参数
@Documented
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface XssProtection {
}
自定义注解处理
为了确保我们的注解生效,我们需要在控制器方法参数上正确处理 @XssProtection 注解。一个有效的方法是通过自定义参数解析器。
自定义参数解析器 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;
}
}
创建过滤器
接下来,我们实现一个过滤器,读取请求的内容,并进行 XSS 清理。
创建过滤器 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 配置类中注册该过滤器或参数解析器:
配置类 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); // 优先级最高
}
}
@Bean
@ConditionalOnProperty(name = "xss.type", havingValue = "filter")
public FilterRegistrationBean<XssFilter> xssFilterRegistrationBean() {
FilterRegistrationBean<XssFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new XssFilter());
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
创建控制器
创建一个简单的控制器来演示我们的 XSS 保护方案:
控制器类 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";
}
}
创建前端页面
创建一个 Thymeleaf 模板页面,用于展示和提交数据。
index.html 页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>XSS 防护示例</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet"/>
<style>
body {
padding-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<h2 class="text-center">XSS 防护示例</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>
代码详细讲解
- 自定义注解:注解 @XssProtection 用于标记我们希望保护的控制器参数。此注解没有实际功能,但在参数解析器中将使用它来判断哪些参数需要进行 XSS 过滤。
- 过滤器:XssFilter 过滤器会以 XssHttpServletRequestWrapper 包装请求对象。这一包装对象的作用是读取请求体并进行 XSS 清理。我们使用了 Apache Commons Text 提供的方法来转义 HTML 字符,防止恶意脚本注入。
- 参数解析器:XssProtectionResolver 自定义参数解析器在控制器方法被调用前处理标记有 @XssProtection 注解的参数。参数解析器使用 StringEscapeUtils.escapeHtml4 方法对参数值进行 HTML 转义,去除潜在的 XSS 攻击向量。
- 前端页面:前端页面使用 Thymeleaf 模板引擎,结合 Bootstrap 框架来创建一个简单的展示页面。用户提交的文本会被展示在页面上,且变化后的内容会通过 XssProtectionResolver 进行 XSS 处理后再显示。
- 配置类:在配置类中注册自定义过滤器和参数解析器,确保它们在项目启动时生效。
1. 正常文本
输入: Hello World
期望输出: Hello World
说明: 正常文本应保持不变,因为它不包含任何潜在的恶意内容。
2. 简单的 HTML 标签
输入: <b>Hello</b>
期望输出: <b>Hello</b>
说明: 为了防止 HTML 注入,将标签内容转义为实体,确保其不会被浏览器解释为实际的 HTML。
3. JavaScript 注入
输入: <script>alert('XSS');</script>
期望输出: <script>alert('XSS');</script>
说明: 转义 <script> 标签及其内容,防止脚本注入并在浏览器中执行。
4. URL 注入
输入: <a href="http://example.com">Click me</a>
期望输出: <a href="http://example.com">Click me</a>
说明: 链接内容应当被转义,防止恶意链接注入并自动执行。
5. 恶意属性
输入: <img src="x" notallow="alert('XSS')">
期望输出: <img src="x" notallow="alert('XSS')">
说明: 移除或转义潜在的恶意属性,如 onerror,以防止利用属性注入执行恶意代码。
总结
通过上述配置,我们创建了一个包含完整 XSS 防护的 Spring Boot 应用。自定义注解、过滤器和参数解析器的结合使我们的解决方案灵活且易于扩展。此方案不仅能有效防止 XSS 攻击,还能保证应用的可维护性和代码的整洁度。