Spring Boot 项目接口 XSS 漏洞处理

status
Published
type
Post
slug
spring-boot-api-xss-fix
date
Apr 7, 2022
tags
Java
Spring
XSS
summary
本文介绍了在 Spring Boot 项目中避免 XSS 漏洞的几种方式。首先,可以使用 Spring 框架提供的 HtmlUtils 类对特殊字符进行转义处理。其次,可以使用 Jsoup 库对 HTML 标签进行过滤,保留需要的标签并删除不必要的标签。另外,还可以通过装饰者模式结合过滤器或拦截器对请求参数进行过滤转义。最后,对于 JSON 形式传参的场景,可以定义一个全局的 JSON 反序列化器来进行处理。总之,通过这些方式可以有效地避免 Spring Boot 项目中的 XSS 漏洞。

起因

项目被扫描了一个 XSS 漏洞风险,接口代码如下:
@RequestMapping("/redirect") public void sendRedirect(HttpServletResponse response, @RequestParam(name = SPRING_SECURITY_FORM_USERNAME_KEY) String userName, @RequestParam(name = SPRING_SECURITY_FORM_PASSWORD_KEY) String password, @RequestParam(name = LOGIN_URL) String redirectUrl, HttpServletRequest request) throws IOException { //... }
这个接口接收了 userName, password, redirectUrl 三个参数用于登录重定向。即用户可以在参数中注入恶意代码,从而实现跨站脚本攻击(XSS)。
例如 构造以下攻击 URL,就会执行对应的 Script 。
.../redirect?redirect_url=&password=&username="><script>alert( "xss")</script>

XSS

XSS 漏洞是指攻击者通过在网页中注入恶意代码,使得用户在访问网页时执行这些恶意代码,从而达到攻击的目的。XSS 攻击主要有以下两种形式:
  • 存储型 XSS 攻击:攻击者将恶意代码存储到数据库中,并在网页中显示出来。当用户访问这个网页时,恶意代码就会被执行。
  • 反射型 XSS 攻击:攻击者将恶意代码作为参数传递给网页,网页在响应时将这些参数输出到页面上。当用户访问这个网页时,恶意代码就会被执行。
XSS 攻击的危害非常大,攻击者可以通过 XSS 攻击偷取用户的敏感信息(如用户名、密码、银行卡号等)、篡改网页内容、重定向用户到恶意网站等。

修复

  • 使用 Spring 框架提供的 HtmlUtils 类对特殊字符进行转义处理,从而防止注入攻击。示例代码如下:
@RequestMapping("/redirect") public void sendRedirect(HttpServletResponse response, @RequestParam(name = SPRING_SECURITY_FORM_USERNAME_KEY) String userName, @RequestParam(name = SPRING_SECURITY_FORM_PASSWORD_KEY) String password, @RequestParam(name = LOGIN_URL) String redirectUrl, HttpServletRequest request) throws IOException { // 使用 HtmlUtils 过滤特殊字符 String userName = HtmlUtils.htmlEscape(userName); String password = HtmlUtils.htmlEscape(password); String redirectUrl = HtmlUtils.htmlEscape(redirectUrl); // ... }
  • 使用 Jsoup 库过滤 HTML 标签,保留需要的标签并删除不必要的标签。Whitelist.none() 对所有输入参数进行了白名单过滤,这意味着不允许任何 HTML 标签和属性。如果需要允许某些标签和属性,请使用更具体的白名单。
// 对所有输入参数使用Jsoup进行白名单过滤 userName = Jsoup.clean(userName, Whitelist.none()); password = Jsoup.clean(password, Whitelist.none()); redirectUrl = Jsoup.clean(redirectUrl, Whitelist.none()); // ...

扩展

装饰者模式(HttpServletRequestWrapper) + 过滤器 (Filter

装饰者模式定义: 动态将责任附加到对象上。想要扩展功能,装饰者提供有别于继承的另一种选择。
装饰者可以在被装饰者的行为前面与/或后面加上自己的行为,甚至将被装饰者的行为整个取代掉,而达到特定的目的。
新建 ParameterRequestWrapper继承 HttpServletRequestWrapper ,并且在 getParameter()getParameterValues()方法中对参数通过 HtmlUtils.htmlEscape() 进行了过滤转义。
public class ParameterRequestWrapper extends HttpServletRequestWrapper { public ParameterRequestWrapper(HttpServletRequest request) { super(request); } @Override public String getParameter(String name) { String value = super.getParameter(name); if (value == null) { return null; } return HtmlUtils.htmlEscape(value, "UTF-8"); } @Override public String[] getParameterValues(String name) { String[] values = super.getParameterValues(name); if (values == null) { return null; } int length = values.length; String[] escapseValues = new String[length]; for (int i = 0; i < length; i++) { escapseValues[i] = HtmlUtils.htmlEscape(values[i], "UTF-8"); } return escapseValues; } @Override public Enumeration<String> getParameterNames() { Enumeration<String> enumeration = super.getParameterNames(); List<String> list = new ArrayList<String>(); while (enumeration.hasMoreElements()) { String name = enumeration.nextElement(); list.add(name); } return Collections.enumeration(list); } }
新建一个名为 xssFilter的过滤器,并将它应用到所有的URL上。在 doFilter()方法中,我们使用上面创建的 ParameterRequestWrapper对请求进行包装。
@WebFilter(filterName = "xssFilter", urlPatterns = "/*") public class XssFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { // 初始化操作 } @Override public void destroy() { // 销毁操作 } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 对参数进行过滤 HttpServletRequest httpRequest = (HttpServletRequest) request; ParameterRequestWrapper wrapper = new ParameterRequestWrapper(httpRequest); chain.doFilter(wrapper, response); } }
需要注意的是,这种方式有可能会对一些合法的输入进行误判,例如用户希望输入一些特殊字符或者HTML标签。故有需求时还需根据实际自行更改其中的实现。
 

拦截器Intercepter

拦截器的实现与过滤器类似,不同的是拦截器可以更加细粒度地控制请求的处理过程。
首先,创建一个拦截器类,该类需要实现HandlerInterceptor接口,并重写preHandle()方法。preHandle()方法在请求到达目标 Controller 之前被调用,可以在此方法中进行参数过滤。
public class XssInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Map<String, String[]> parameterMap = request.getParameterMap(); for (String key : parameterMap.keySet()) { String[] values = parameterMap.get(key); for (int i = 0; i < values.length; i++) { values[i] = HtmlUtils.htmlEscape(values[i], "UTF-8"); } request.getParameterMap().put(key, values); } return true; } }
然后,需要在 Spring Boot 应用程序中注册该拦截器。可以通过在配置类中添加@Bean注解来创建拦截器实例,并通过addInterceptors()方法将拦截器添加到WebMvcConfigurer中。
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Bean public XssInterceptor xssInterceptor() { return new XssInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(xssInterceptor()).addPathPatterns("/**"); } }
 

JSON 反序列化器 (适用于 JSON 形式传参)

前后端分离场景下,基本都是通过 @RequestBody 注解接收 application/json 格式的请求体,所以上述 装饰者模式+过滤器 方式不再适用。我们可以直接定义一个全局的 JSON 反序列化器来进行处理。
@Component public class StringJsonDeserializer extends JsonDeserializer<String> { @Override public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { String text = jsonParser.getText(); if (StringUtils.isNotBlank(text)) { return HtmlUtils.htmlEscape(text); } else { return null; } } }
将其注册到 Spring 的 Jackson 配置中
@Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper() { ObjectMapper objectMapper = new ObjectMapper(); SimpleModule module = new SimpleModule(); module.addDeserializer(String.class, new StringJsonDeserializer()); objectMapper.registerModule(module); return objectMapper; } }
这样,当从前端接收到 JSON 数据并进行反序列化时,所有的字符串类型都会被自动转义,也就避免了 XSS 漏洞。

Tips

以上为对本次案例代码以及相应其他场景的解决方案。针对 XSS 漏洞,注意以下几点:
  1. 输入过滤和验证:对于用户输入的数据,要进行过滤和验证,防止恶意输入,比如对于特殊字符要进行转义或者删除。
  1. 数据输出转义:对于输出到 HTML 页面的数据,要进行转义,比如将 < 转义成 &lt;,将 > 转义成 &gt; 等,这样就能避免 XSS 攻击。
  1. 使用 HTTP-only Cookie:将 Cookie 设置为 HTTP-only,这样浏览器就无法通过 JavaScript 访问 Cookie。
  1. CSP(Content Security Policy):开启 CSP 可以限制页面加载资源的来源,包括 JS、CSS、图片等。CSP 限制了页面中可执行的 JavaScript。