Spring官方发布了CVE-2023-34035,当应用程序使用了 requestMatchers(String)
和多个 Servlet(其中一个是 Spring MVC 的 DispatcherServlet)的情况下,Spring Security漏洞版本存在可能受到授权规则配置错误的影响。
以SpringSecurity 5.8.4为例,查看具体的原理。查看requestMatchers的具体实现:
首先根据是否存在SpringMVC,来选择不同的方式创建RequestMatcher。如果存在 SpringMVC的话,则创建MvcRequestMatcher,否则创建AntPathRequestMatcher:
根据漏洞利用条件,可以知道这里创建的是MvcRequestMatcher。
在SpringSecurity中,会通过org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager#check 方法通过遍历请求匹配器列表,根据请求的 URL 和 HTTP 方法来决定哪个授权管理器应该处理该请求,并最终决定是否授予或拒绝对该请求的访问权限:
因为这里创建的是MvcRequestMatcher,所以直接查看org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher#matcher的具体实现。
首先接收request对象,然后调用 this.notMatchMethodOrServletPath(request) 方法来检查当前请求的 HTTP 方法和 Servlet 路径是否与预定义的条件匹配。如果不匹配,将返回 MatchResult.notMatch() 表示不匹配:
否则调用this.getMapping(request)
方法获取请求的处理器映射对象(MatchableHandlerMapping
)。如果不存在与当前请求相匹配的处理器映射,则调用 this.defaultMatcher.matcher(request)
方法,使用默认的请求匹配器来进行匹配,否则使用请求匹配器和预定义的 URL 模式来进行匹配,即调用 mapping.match(request, this.pattern) 方法:
最后,根据匹配结果来返回相应的 MatchResult 对象。如果请求匹配成功,则返回 MatchResult.match(result.extractUriTemplateVariables()),其中 result.extractUriTemplateVariables() 用于提取匹配的路径变量。否则,返回 MatchResult.notMatch() 表示不匹配。
继续查看mapping.match(request, this.pattern) 的解析过程,这里会使用请求匹配器来进行请求匹配,得到匹配结果 RequestMatchResult
:
在match方法中,首先获取当前请求的路径,并将其表示为 PathContainer
对象,然后通过PathPattern的方式来匹配当前请求的路径:
这里path的获取主要是从pathWithinApplication属性获取,主要是fullPath和contextPath属性决定的:
在处理contextPath属性时,servlet跟SpringMVC endpoint直接会有差异。主要在ServletRequestPathUtils.ServletRequestPath.parse中:
整个解析的过程比较复杂,通过一个实际的例子来说明:
@Controller
@RequestMapping("/admin/")
public class AdminController {
@GetMapping("/*")
public String Manage(){
/*return "Manage page";*/
return "manage";
}
}
默认情况下,当通过请求/admin/page解析上述Controller时,在调用getServletPathPrefix方法时返回为null,此时contextPath属性为"":
返回的当前请求的路径为/admin/page。
而对于自定义Servlet来说:
@WebServlet(urlPatterns = "/admin/*")
public class UserServlet extends HttpServlet{
private static final long serialVersionUID = 1L;
@Override
public void init() throws ServletException {
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
System.out.println(req.getContextPath());
resp.getWriter().write("user page");
return ;
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
this.doGet(req, resp);
}
@Override
public void destroy() {
}
}
当通过请求/admin/page解析时,在调用getServletPathPrefix方法时返回为admin,此时contextPath属性为admin:
此时返回的当前请求的路径为/page:
但是对于SpringSecurity来说,理论上requestMatchers("/admin/**").hasRole("ADMIN")都应该能覆盖上述两个资源。根据前面的分析,在match方法中,在获取当前请求的路径后,通过PathPattern的方式来匹配当前请求的路径时servlet明显会获取不到,这里存在解析差异导致意料之外的结果。
根据前面的分析,主要是MvcRequestMatcher对于自定义Servlet端点的解析存在差异,导致Authorization规则可能与预期不一致的问题。以SpringSecurity 5.8.4为例,下面看一个具体的例子:
假设当前配置类的安全规则如下,对于admin路径下的资源要求用户具有"ADMIN"权限,对于manage路径下的资源要求用户具有"MANAGE"权限,而对于其他任意请求路径,则直接放行:
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().requestMatchers("/admin/**").hasRole("ADMIN").requestMatchers("/manage/**").hasRole("MANAGE").anyRequest().permitAll();
return http.build();
}
}
其中应用注册的资源如下,首先是SpringMVC的endpoint:
@Controller
public class ManageController {
@GetMapping("/manage/page")
public String Manage(){
/*return "Manage page";*/
return "manage";
}
}
然后是通过@WebServlet注解自定义的UserServlet,这里urlPatterns定义成了/admin/*
,代表以 "/admin/" 开头的所有 URL 请求都会由这个 Servlet 来处理:
@WebServlet(urlPatterns = "/admin/*")
public class UserServlet extends HttpServlet{
private static final long serialVersionUID = 1L;
@Override
public void init() throws ServletException {
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.getWriter().write("user page");
return ;
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
this.doGet(req, resp);
}
@Override
public void destroy() {
}
}
按照前面SpringSecurity的配置,正常情况下这两个资源在未授权的情况下访问,预期应该都会返回403 status。
可以看到这里对于/admin/**
的防护,并没有生效:
通过对比SpringSecurity 5.8.4与5.8.5,可以发现关键的修复代码主要在org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry#requestMatchers
,该方法主要用于匹配对应的RequestMatcher。
这里主要是根据是否存在SpringMVC,来选择不同的方式创建RequestMatcher。如果存在 SpringMVC的话,则创建MvcRequestMatcher,否则创建AntPathRequestMatcher:
可以看到在5.8.5版本,对应的方法做了比较大的改动。首先检查是否存在SpringMVCmvcPresent 是否为 true以及应用程序是否使用了 WebApplicationContext,如果不满足条件会创建AntPathRequestMatcher。
如果存在SpringMVC并且使用了WebApplicationContext,则获取WebApplicationContext对象,并从中获取 ServletContext。如果 ServletContext 为null同样会创建AntPathRequestMatcher。
如果存在 ServletContext,则检查其中注册的 Servlet。如果不存在任何 Servlet 或没有 Spring MVC 的 DispatcherServlet,同样会创建AntPathRequestMatcher。
如果只有一个注册的 Servlet,并且是 Spring MVC 的 DispatcherServlet,那么将调用 this.createMvcMatchers(method, patterns) 方法创建 MVC 请求匹配器,也就是创建MvcRequestMatcher。如果包含多个Servlet的情况(registrations的size大于1),会抛出异常并打印相应的错误信息,对于非Spring MVC endpoint,请使用AntPathRequestMatcher进行对应的配置:
This method cannot decide whether these patterns are Spring MVC patterns or not.
If this endpoint is a Spring MVC endpoint, please use `requestMatchers(MvcRequestMatcher)`;
otherwise, please use `requestMatchers(AntPathRequestMatcher)`.
查看具体的效果,同样是上面的例子,此时registrations的size大于1,会抛出对应的异常:
同样是上面的案例,当使用AntPathRequestMatcher处理/admin/**请求后,自定义的Servlet安全防护符合预期:
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().requestMatchers(new AntPathRequestMatcher("/admin/**")).hasRole("ADMIN").requestMatchers("/manage/**").hasRole("MANAGE").anyRequest().permitAll();
return http.build();
}
}
64 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!