SpringWeb常见鉴权措施与垂直越权检测
一、越权测试中的痛点/难点
越权漏洞是日常开发中比较常见的一个缺陷。要进行越权检测,一般需要明确定义和管理系统中的权限。这可能包括用户角色、资源和操作的细粒度权限控制。维护这些权限定义并确保它们与实际业务操作一致本身就是是一项复杂的任务。
以垂直越权为例,一般测试时,首先会获取到高权限用户模块的业务数据包,然后在BurpSuite将其鉴权凭证(一般是cookie)替换成低权限用户的,也可以构造好高权限的业务数据包,然后在浏览器登陆低权限用户,通过访问相关的数据包,若业务依旧可以成功访问,则存在垂直越权缺陷。但是实际测试过程中可能会遇到一系列的问题:
- 在测试过程中,了解应用程序的请求接口和其参数是至关重要的。这包括了解哪些参数控制了访问权限、哪些参数可以被滥用,以及如何构造恶意请求。但是在实际测试时,经常是通过js代码审计发现潜在的垂直越权接口,请求参数信息可能不完整或模糊导致没办法进步一测试(这些信息可能不容易获取,因为无法获取应用程序的源代码,也无法直接查看其接口文档)。
- 一些越权漏洞可能导致误删除/修改/更新操作,在没有测试环境的情况下数据恢复比较困难。
- 在黑盒扫描中,需要获取足够的应用程序流量数据,以便进行分析和检测。然而,流量可能受到加密、访问权限和网络配置的限制。
除此以外,还有还多其他的因素,下面先看看SpringWeb中常见鉴权措施与解析顺序,简单探索下能不能在一定程度上解决上述的问题。
二、SpringWeb中常见鉴权措施
2.1 过滤器Filter
过滤器是位于请求处理链的最外层,可以拦截请求并进行对应的处理。如果某资源已经配置对应filter进行处理的话,那么每次访问这个资源都会执行doFilter()方法,该方法也是过滤器的核心方法。例如可以在调用目标资源之前,进行权限等的处理。
过滤器是Servlet的实现规范,仅在tomcat等Web容器中调用。Spring Boot默认内嵌Tomcat作为Web服务器。以tomcat-embed-core-9.0.64为例,查看Filter的具体调用过程。
Filter调用时会在org.apache.catalina.cor.StandardWrapperValve#invoke()方法中被创建执行。主要是通过ApplicationFilterFactory.createFilterChain创建FilterChain:
查看createFilterChain方法的具体实现:
首先会检查 servlet是否为null,如果是表示没有指定Servlet,就没有需要创建的过滤器链。否则根据不同的情况创建一个 ApplicationFilterChain
对象或获取已存在的过滤器链对象。过滤器链对象负责管理一系列的过滤器:
然后获取所有的filter的映射对象,在filterMaps中保存的是各个filter的元数据信息,若filterMaps不为null且length不为0,会对前面创建的filterChain进一步的封装,首先会获取与当前请求相关的标识信息,例如请求的调度类型(dispatcher)和请求的路径(requestPath):
然后遍历所有过滤器映射,根据一定的条件判断将匹配的过滤器添加到过滤器链中。条件包括与调度类型的匹配和与请求路径或Servlet名称的匹配:
最后,返回创建的过滤器链,该过滤器链包含了所有匹配的过滤器。如果没有找到匹配的过滤器,则返回一个空的过滤器链。创建了filterChain之后,就开始执行ApplicationFilterChain的doFilter进行请求的链式处理:
具体的逻辑在org.apache.catalina.core.ApplicationFilterChain#internalDoFilter方法,首先通过pos索引判断是否执行完了所有的filter,如果没有,取出当前待执行的索引filter,调用其doFilter方法:
当所有的filter执行完后,会释放掉过滤器链及其相关资源。开始执行servlet业务模块servlet.service(request, response);
以上是tomcat中整个Filter的调用过程。而 Controller 中收到的请求,都是经过 Tomcat 容器解析后交给 DispatcherServlet
,再由其转交给对应 Controller 的。
所以,过滤器是在Servlet容器级别处理请求的,因此会在Spring框架内部的其他组件之前执行 。
看一个实际的例子,验证前面的结论:
这里通过FilterRegistrationBean实例进行注册,将自定义的 AuthFilter 声明成 Bean 交给 Spring 管理,在AuthFilter中对/admin/**
目录下的资源进行了权限控制:
@Bean
public FilterRegistrationBean<AuthFilter> FilterConfig() {
FilterRegistrationBean<AuthFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new AuthFilter());
registrationBean.addUrlPatterns("/admin/*");
return registrationBean;
}
假设/admin存在如下更新用户的接口:
@RequestMapping(value="/user/add",method = RequestMethod.POST)
public ApiResponse<User> addUser (@RequestBody User user){
return new ApiResponse<>(200, "Success", userService.saveOrUpdate(user));
}
该接口使用POST进行请求,通过@RequestBody的方式进行数据的传输。未经过任何类似鉴权filter方式处理的话,若通过如下方式进行请求会返回相应的状态码:
- 当使用GET方式请求该接口时,会返回405 Status
- 当通过
application/x-www-form-urlencoded;charset=UTF-8
方式请求该接口时,会返回415 Status
- 当通过
application/json
方式请求,但是传递参数为null时,会返回400 Status:
因为过滤器会在Spring框架内部的其他组件之前执行,所以当AuthFilter逻辑生效时,前面的请求方式均会返回403 Status,这里以GET请求为例:
2.2 拦截器Interceptor( preHandle )
拦截器是Spring框架提供的机制,用于在请求处理的不同阶段(如处理器方法前后、视图渲染前后)执行自定义逻辑。可以通过创建自定义拦截器,进行检查请求、修改模型数据或执行其他操作。
实现拦截器需要实现HandlerInterceptor这个接口,这个接口中有三个默认方法,这三个方法的执行顺序:
- preHandle:Controller方法执行之前执行preHandle(),返回值是一个boolean,表示是否拦截或放行,返回true为放行,即调用控制器方法;返回false表示拦截,即不调用控制器方法
- postHandle:Controller方法执行之后执行postHandle()
- afterComplation:处理完视图和模型数据,渲染视图完毕之后执行afterComplation()
在鉴权场景中,拦截器常常用于在请求处理前执行,也就是一般会调用preHandle,后续仅讨论preHandle的情况 。
过滤器是在DispatcherServlet
处理之前拦截,而拦截器是在DispatcherServlet
处理请求然后调用Controller方法之前进行拦截。所以具体查看DispatcherServlet
是怎么对拦截器进行处理的:
当Spring MVC接收到请求时,Servlet容器会调用DispatcherServlet的service方法。这里会调用doDispatch方法进行进一步的处理。来获取对应的mappedHandler:
在getHandler方法中,会顺序循环调用HandlerMapping的getHandler方法:
首先会通过RequestMappingHandlerMapping处理,在其getHandler方法中通过getHandlerInternal获取handler构建HandlerExecutionChain并返回,这里会添加该请求相关的所有Interceptor:
在getHandlerExecutionChain方法中,首先会创建一个HandlerExecutionChain
对象,用于存储处理器和拦截器。这里会遍历 adaptedInterceptors
的拦截器集合,如果拦截器是 MappedInterceptor
的实例,并且它的 matches(request)
方法返回 true
(表示请求的URL路径匹配该拦截器),则将该拦截器中的实际拦截器添加到 chain
中。否则直接将它添加到 chain
中,无需进行路径匹配:
最后会返回构建好的 HandlerExecutionChain
对象 chain
,其中包含了处理程序和相应的拦截器,以便在处理HTTP请求时按照一定的顺序执行这些拦截器操作。处理完后会获取处理器适配器,然后调用applyPreHandle方法进行处理:
这里实际就是执行拦截器前置处理preHandle方法:
后续会执行具体Controller下的服务,以及执行HandlerInterceptor的PostHandle和AfterCompletion方法,这里也映证了preHandle会在Controller方法执行之前执行:
以上是拦截器Interceptor的大致执行流程。
看一个实际的例子,验证前面的结论:
通过实现HandlerInterceptor 接口自定义拦截器AuthInterceptor:
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 在请求处理前执行,可进行鉴权逻辑
// 获取当前用户信息
String currentUser = getCurrentUser(request);
// 进行垂直越权检测
if (!hasPermission(currentUser)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return false; // 拒绝访问
}
return true; // 允许访问
}
private String getCurrentUser(HttpServletRequest request) {
// 实现获取当前用户的逻辑
}
private boolean hasPermission(String currentUser, String resourceId) {
// 实现垂直越权检测逻辑
}
}
在配置类添加@Configuration注解,通过重写addInterceptors方法,添加拦截器,并配置匹配路径,AuthInterceptor对/admin/目录下的所有资源都生效,也就是说这是一个垂直鉴权的措施:
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器
registry.addInterceptor(new AuthInterceptor(environment)).addPathPatterns("/admin/**").excludePathPatterns("/test/**");
}
同样是前面过滤器Filter中访问/user/add接口的例子,当拦截器AuthInterceptor生效时,再次使用对应的方式请求会返回403 Status,说明不存在对应的身份无法访问:
使用拦截器preHandle进行鉴权时,对应的解析逻辑会在Controller方法执行之前执行。与Controller的参数、Content-Type发送的数据类型无关。 主要是检查请求是否具备足够的权限访问某个资源,而不是检查请求的具体内容 。
但是要注意一点,跟过滤器Filter不同的是,类似上面的案例,当拦截器AuthInterceptor生效时,使用GET方式请求/admin/user/add(POST请求接口)接口并不会返回403 Stauts:
这是因为在RequestMappingHandlerMapping的getHandler方法中通过getHandlerInternal获取handler时,会对请求匹配对应的handler,其中一处就是请求Method的匹配,若资源不存在的话,也没必要进一步处理了:
2.3 切面
切面是Spring的AOP(面向切面编程)组件,用于定义横切关注点。虽然它们通常用于横切关注点的日志记录、性能监控等,但也可以用于鉴权。你可以创建自定义切面,它们可以拦截方法调用,以执行鉴权逻辑。
相比其他措施, 切面的执行顺序十分灵活,可以通过配置进行管理,它们可以被指定在其他组件之前或之后执行 。
最常见的是通过注解方式来实现,主要包含以下注解:
- @Aspect : 指定切面类(可以通过检索@Aspect快速定位切面类)
- @Pointcut :定义了相应的 Advice(具体要做的操作)触发的地方。一般是通过通配符、正则表达式等方式。
例如如下的案例,代码中只要被@Auth注解标记的方法均会执行当前切面定义的内容:
@Pointcut("@annotation(Auth)")
public void requirePermissionAuth() {
- 通知(Advice)类型:定义要执行的方法,安全中一般是鉴权
- @Before: 在目标方法执行之前,执行注解标记的内容
- @After: 在目标方法执行之后,执行注解标记的内容
- @AfterReturning: 在目标方法返回后,执行注解标记的内容
- @AfterThrowing: 在目标方法抛出异常后,执行注解标记的内容
- @Around: 在目标方法执行前后,分别执行对应的内容
在 权限校验中,比较常用的是@Before和@Around 。例如@Around,在判断权限之后选择对应的函数是否执行。如果权限满足,那么执行函数,如果不满足直接抛出权限不足的提示。
相比Filter和Interceptor, 切面在方法级别执行 ,并且可以拿到Controller方法的参数进行操作。
2.4 在 Service 层实施鉴权
除了上面的方式以外,如果对应鉴权规则是基于业务逻辑的,一般还会在 Service 层实现鉴权。 Service 层通常包含业务逻辑和数据访问,可以更好地控制鉴权逻辑。与路径级别的鉴权不同,在 Service 层实施鉴权可以确保所有相关的业务方法都经过相同的鉴权规则。
例如下面的例子,UserService中包含了一个hasPermissionToDeleteUser方法,该方法根据自定义的鉴权逻辑来检查是否允许当前用户删除指定的用户。然后,在deleteUser方法中,我们检查是否有权限删除用户,并根据鉴权结果来执行删除用户的业务逻辑或抛出异常:
@Service
public class UserService {
public boolean hasPermissionToDeleteUser(User currentUser, User userToDelete) {
// 自定义鉴权逻辑,检查是否允许当前用户删除指定用户
if (currentUser.isAdmin()) {
return true; // 管理员可以删除任何用户
} else {
return currentUser.getId().equals(userToDelete.getId()); // 用户只能删除自己
}
}
public User deleteUser(User currentUser, User userToDelete) {
if (hasPermissionToDeleteUser(currentUser, userToDelete)) {
// 执行删除用户的业务逻辑
return userToDelete;
} else {
throw new SecurityException("没有权限删除用户");
}
}
// 其他方法...
}
因为Service的调用一般都是在Controller,此时已经完成路径解析&匹配了,相比于前面的措施,基本上是最后才进行解析。
三、鉴权措施的执行顺序
根据上面的分析,可以大概知道,当一个请求到达时,执行顺序是:Filter过滤器> Interceptor拦截器> ControllerAdvice > AOP > Controller,在Controller之后,就是具体的service调用了。
四、简单的垂直越权检测
对于垂直越权的场景,一般情况下在系统设计开发时,会根据路由进行权限角色的区分,过拦截器(Interceptor)或过滤器(Filter)之类的中间件组件来保护应用程序。
以SpringSecurity为例,Spring Security内部其实是通过一个过滤器链来实现认证/鉴权等流程的。例如下面的例子,这里限制了/admin/以及/manage目录下的接口均需要ADMIN角色才能进行访问:
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().requestMatchers("/admin/**","/manage/**").hasRole("ADMIN").anyRequest().permitAll();
return http.build();
}
}
根据前面的分析,也就是说不论/admin下的接口参数是否已知,是否有完整的请求流量,都可以通过直接请求接口的方式进行验证,若返回接口不是403,则可能存在垂直越权的风险:
对于基于Spring开发系统,可以结合鉴权措施的执行顺序,在一定程度上解决垂直越权测试上的问题,但是对于平行越权来说,一般情况下的防护措施是基于业务逻辑的,一般会在 Service 层实现鉴权。这里就必须获取到对应的请求参数了或者完整的流量了。