本文旨在探讨如何在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攻击:
攻击者在一个博客评论系统中提交以下评论:
当其他用户查看这条评论时,他们的cookie会被发送到攻击者的服务器。
- 反射型XSS攻击:
攻击者构造一个恶意URL:
http://example.com/search?q=
如果服务器直接将搜索词嵌入到响应中而不进行过滤,用户点击此链接后会看到一个警告框。
- DOM型XSS攻击:
假设网页中有以下JavaScript代码:
var name = document.location.hash.substr(1);
document.write("欢迎, " + name);
攻击者可以构造如下URL:
http://example.com/page.html#
当用户访问此URL时,恶意脚本会被执行。
二、Spring Boot中的XSS防御手段
在Spring Boot中,我们可以采用多种方式来防御XSS攻击。下面将详细介绍两种常用的防御手段:使用注解和使用过滤器。
2.1 使用注解进行XSS防御
注解是一种轻量级的防御手段,它可以在方法或字段级别对输入进行校验,从而防止XSS攻击。
2.1.1 引入相关依赖
org.springframework.boot
spring-boot-starter-validation
3.2.0
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 {
private static final Safelist WHITE_LIST = Safelist.relaxed();
private static final Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(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 login(@RequestBody @Validated UserLoginDTO userLoginDTO) {
return Result.success();
}
2.2 使用过滤器进行XSS防御
2.2.1 引入相关依赖
org.jsoup
jsoup
1.17.2
2.2.2 编写配置类
@Data
@Component
@ConfigurationProperties(prefix = "xss")
public class FilterConfig {
private String enabled;
private String excludes;
private String urlPatterns;
@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 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:
private List excludes = new ArrayList<>();
private boolean enabled = false;
@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);
}
}
@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() {
// 无需额外的销毁逻辑
}
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 编写过滤工具类
public class XssUtil {
private static final Safelist WHITE_LIST = Safelist.relaxed();
private static final Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);
static {
// 富文本编辑时一些样式是使用 style 来进行实现的
// 比如红色字体 style="color:red;"
// 所以需要给所有标签添加 style 属性
WHITE_LIST.addAttributes(":all", "style");
}
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 {
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);
}
@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);
}
private String cleanXSS(String value) {
return XssUtil.clean(value);
}
}
2.2.7 自定义json消息解析器
在使用springboot中,类似于普通的参数parameter,attribute,header一类的,可以直接使用过滤器来过滤。而前端发送回来的json字符串就没那么方便过滤了。可以考虑用自定义json消息解析器来过滤前端传递的json。
public class XSSMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
@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;
}
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);
}
}
@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注解:
如果不符合规则的字符(例如)会提示非法输入,检测到潜在的XSS,可以看到下面的返回参数中的message已经变为默认警告。
图片
3.2 XSS过滤器
XSS过滤器实现的效果是过滤,将前端传递参数进行清理,达到XSS防御的目的。
观察下面的测试结果可以知道过滤器成功实现参数清理。
图片
图片
四、总结
本文深入探讨了在Spring Boot应用程序中如何有效地防御XSS攻击。我们介绍了两种主要的防御手段:使用注解和使用过滤器。