SpringWeb常見鑒權(quán)措施與垂直越權(quán)檢測
一、越權(quán)測試中的痛點(diǎn)/難點(diǎn)
越權(quán)漏洞是日常開發(fā)中比較常見的一個缺陷。要進(jìn)行越權(quán)檢測,一般需要明確定義和管理系統(tǒng)中的權(quán)限。這可能包括用戶角色、資源和操作的細(xì)粒度權(quán)限控制。維護(hù)這些權(quán)限定義并確保它們與實際業(yè)務(wù)操作一致本身就是是一項復(fù)雜的任務(wù)。
以垂直越權(quán)為例,一般測試時,首先會獲取到高權(quán)限用戶模塊的業(yè)務(wù)數(shù)據(jù)包,然后在BurpSuite將其鑒權(quán)憑證(一般是cookie)替換成低權(quán)限用戶的,也可以構(gòu)造好高權(quán)限的業(yè)務(wù)數(shù)據(jù)包,然后在瀏覽器登陸低權(quán)限用戶,通過訪問相關(guān)的數(shù)據(jù)包,若業(yè)務(wù)依舊可以成功訪問,則存在垂直越權(quán)缺陷。但是實際測試過程中可能會遇到一系列的問題:
- 在測試過程中,了解應(yīng)用程序的請求接口和其參數(shù)是至關(guān)重要的。這包括了解哪些參數(shù)控制了訪問權(quán)限、哪些參數(shù)可以被濫用,以及如何構(gòu)造惡意請求。但是在實際測試時,經(jīng)常是通過js代碼審計發(fā)現(xiàn)潛在的垂直越權(quán)接口,請求參數(shù)信息可能不完整或模糊導(dǎo)致沒辦法進(jìn)步一測試(這些信息可能不容易獲取,因為無法獲取應(yīng)用程序的源代碼,也無法直接查看其接口文檔)。
- 一些越權(quán)漏洞可能導(dǎo)致誤刪除/修改/更新操作,在沒有測試環(huán)境的情況下數(shù)據(jù)恢復(fù)比較困難。
- 在黑盒掃描中,需要獲取足夠的應(yīng)用程序流量數(shù)據(jù),以便進(jìn)行分析和檢測。然而,流量可能受到加密、訪問權(quán)限和網(wǎng)絡(luò)配置的限制。
除此以外,還有還多其他的因素,下面先看看SpringWeb中常見鑒權(quán)措施與解析順序,簡單探索下能不能在一定程度上解決上述的問題。
二、SpringWeb中常見鑒權(quán)措施
2.1 過濾器Filter
過濾器是位于請求處理鏈的最外層,可以攔截請求并進(jìn)行對應(yīng)的處理。如果某資源已經(jīng)配置對應(yīng)filter進(jìn)行處理的話,那么每次訪問這個資源都會執(zhí)行doFilter()方法,該方法也是過濾器的核心方法。例如可以在調(diào)用目標(biāo)資源之前,進(jìn)行權(quán)限等的處理。
過濾器是Servlet的實現(xiàn)規(guī)范,僅在tomcat等Web容器中調(diào)用。Spring Boot默認(rèn)內(nèi)嵌Tomcat作為Web服務(wù)器。以tomcat-embed-core-9.0.64為例,查看Filter的具體調(diào)用過程。
Filter調(diào)用時會在org.apache.catalina.cor.StandardWrapperValve#invoke()方法中被創(chuàng)建執(zhí)行。主要是通過ApplicationFilterFactory.createFilterChain創(chuàng)建FilterChain:
wKg0C2UZa3mAcUOHAADOEBt3mHs966.png
查看createFilterChain方法的具體實現(xiàn):
首先會檢查 servlet是否為null,如果是表示沒有指定Servlet,就沒有需要創(chuàng)建的過濾器鏈。否則根據(jù)不同的情況創(chuàng)建一個 ApplicationFilterChain對象或獲取已存在的過濾器鏈對象。過濾器鏈對象負(fù)責(zé)管理一系列的過濾器:
wKg0C2UZa7aAAQAABBWzHMHw483.png
然后獲取所有的filter的映射對象,在filterMaps中保存的是各個filter的元數(shù)據(jù)信息,若filterMaps不為null且length不為0,會對前面創(chuàng)建的filterChain進(jìn)一步的封裝,首先會獲取與當(dāng)前請求相關(guān)的標(biāo)識信息,例如請求的調(diào)度類型(dispatcher)和請求的路徑(requestPath):
wKg0C2UZa8AfjG8AACN0FNuSC8022.png
然后遍歷所有過濾器映射,根據(jù)一定的條件判斷將匹配的過濾器添加到過濾器鏈中。條件包括與調(diào)度類型的匹配和與請求路徑或Servlet名稱的匹配:
wKg0C2UZayAMT7gAAC5eGoKgU526.png
最后,返回創(chuàng)建的過濾器鏈,該過濾器鏈包含了所有匹配的過濾器。如果沒有找到匹配的過濾器,則返回一個空的過濾器鏈。創(chuàng)建了filterChain之后,就開始執(zhí)行ApplicationFilterChain的doFilter進(jìn)行請求的鏈?zhǔn)教幚恚?/p>
wKg0C2UZbAaAJnU6AAArmzqLg216.png
具體的邏輯在org.apache.catalina.core.ApplicationFilterChain#internalDoFilter方法,首先通過pos索引判斷是否執(zhí)行完了所有的filter,如果沒有,取出當(dāng)前待執(zhí)行的索引filter,調(diào)用其doFilter方法:
wKg0C2UZbCCADH5nAACSN2AJnsU546.png
當(dāng)所有的filter執(zhí)行完后,會釋放掉過濾器鏈及其相關(guān)資源。開始執(zhí)行servlet業(yè)務(wù)模塊servlet.service(request, response);
wKg0C2UZbD6AYDHSAABgcy22c8293.png
以上是tomcat中整個Filter的調(diào)用過程。而 Controller 中收到的請求,都是經(jīng)過 Tomcat 容器解析后交給 DispatcherServlet,再由其轉(zhuǎn)交給對應(yīng) Controller 的。
所以,過濾器是在Servlet容器級別處理請求的,因此會在Spring框架內(nèi)部的其他組件之前執(zhí)行。
看一個實際的例子,驗證前面的結(jié)論:
這里通過FilterRegistrationBean實例進(jìn)行注冊,將自定義的 AuthFilter 聲明成 Bean 交給 Spring 管理,在AuthFilter中對/admin/**目錄下的資源進(jìn)行了權(quán)限控制:
@Bean
public FilterRegistrationBean<AuthFilter> FilterConfig() {
FilterRegistrationBean<AuthFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new AuthFilter());
registrationBean.addUrlPatterns("/admin/*");
return registrationBean;
}
假設(shè)/admin存在如下更新用戶的接口:
@RequestMapping(value="/user/add",method = RequestMethod.POST)
public ApiResponse<User> addUser (@RequestBody User user){
return new ApiResponse<>(200, "Success", userService.saveOrUpdate(user));
}
該接口使用POST進(jìn)行請求,通過@RequestBody的方式進(jìn)行數(shù)據(jù)的傳輸。未經(jīng)過任何類似鑒權(quán)filter方式處理的話,若通過如下方式進(jìn)行請求會返回相應(yīng)的狀態(tài)碼:
- 當(dāng)使用GET方式請求該接口時,會返回405 Status
wKg0C2UZbIWAK8kdAACg0gYo8ec207.png
- 當(dāng)通過application/x-www-form-urlencoded;charset=UTF-8方式請求該接口時,會返回415 Status
wKg0C2UZbJAIXY5AACwhbZnFbo493.png
- 當(dāng)通過application/json方式請求,但是傳遞參數(shù)為null時,會返回400 Status:
wKg0C2UZbLKAaIkPAACm5tOsjQ861.png
因為過濾器會在Spring框架內(nèi)部的其他組件之前執(zhí)行,所以當(dāng)AuthFilter邏輯生效時,前面的請求方式均會返回403 Status,這里以GET請求為例:
wKg0C2UZbMOAVoO7AACQPalukXo530.png
2.2 攔截器Interceptor( preHandle)
攔截器是Spring框架提供的機(jī)制,用于在請求處理的不同階段(如處理器方法前后、視圖渲染前后)執(zhí)行自定義邏輯??梢酝ㄟ^創(chuàng)建自定義攔截器,進(jìn)行檢查請求、修改模型數(shù)據(jù)或執(zhí)行其他操作。
實現(xiàn)攔截器需要實現(xiàn)HandlerInterceptor這個接口,這個接口中有三個默認(rèn)方法,這三個方法的執(zhí)行順序:
- preHandle:Controller方法執(zhí)行之前執(zhí)行preHandle(),返回值是一個boolean,表示是否攔截或放行,返回true為放行,即調(diào)用控制器方法;返回false表示攔截,即不調(diào)用控制器方法
- postHandle:Controller方法執(zhí)行之后執(zhí)行postHandle()
- afterComplation:處理完視圖和模型數(shù)據(jù),渲染視圖完畢之后執(zhí)行afterComplation()
在鑒權(quán)場景中,攔截器常常用于在請求處理前執(zhí)行,也就是一般會調(diào)用preHandle,后續(xù)僅討論preHandle的情況。
過濾器是在DispatcherServlet處理之前攔截,而攔截器是在DispatcherServlet處理請求然后調(diào)用Controller方法之前進(jìn)行攔截。所以具體查看DispatcherServlet是怎么對攔截器進(jìn)行處理的:
當(dāng)Spring MVC接收到請求時,Servlet容器會調(diào)用DispatcherServlet的service方法。這里會調(diào)用doDispatch方法進(jìn)行進(jìn)一步的處理。來獲取對應(yīng)的mappedHandler:
wKg0C2UZbXSAa7R1AABPnZI54iE784.png
在getHandler方法中,會順序循環(huán)調(diào)用HandlerMapping的getHandler方法:
wKg0C2UZbZOAcQcAABcsXe4svI564.png
首先會通過RequestMappingHandlerMapping處理,在其getHandler方法中通過getHandlerInternal獲取handler構(gòu)建HandlerExecutionChain并返回,這里會添加該請求相關(guān)的所有Interceptor:
wKg0C2UZbaAWY7EAACWNQfmjc257.png
在getHandlerExecutionChain方法中,首先會創(chuàng)建一個HandlerExecutionChain對象,用于存儲處理器和攔截器。這里會遍歷 adaptedInterceptors的攔截器集合,如果攔截器是 MappedInterceptor的實例,并且它的 matches(request)方法返回 true(表示請求的URL路徑匹配該攔截器),則將該攔截器中的實際攔截器添加到 chain中。否則直接將它添加到 chain中,無需進(jìn)行路徑匹配:
wKg0C2UZbdKAJZRxAACIcsItVQ657.png
最后會返回構(gòu)建好的 HandlerExecutionChain對象 chain,其中包含了處理程序和相應(yīng)的攔截器,以便在處理HTTP請求時按照一定的順序執(zhí)行這些攔截器操作。處理完后會獲取處理器適配器,然后調(diào)用applyPreHandle方法進(jìn)行處理:
wKg0C2UZbemABcz3AAAroZqycWc460.png
這里實際就是執(zhí)行攔截器前置處理preHandle方法:
wKg0C2UZbgSAZ9rkAABr9GOWJU163.png
后續(xù)會執(zhí)行具體Controller下的服務(wù),以及執(zhí)行HandlerInterceptor的PostHandle和AfterCompletion方法,這里也映證了preHandle會在Controller方法執(zhí)行之前執(zhí)行:
wKg0C2UZbhuAWdmhAABeSb0zcas924.png
以上是攔截器Interceptor的大致執(zhí)行流程。
看一個實際的例子,驗證前面的結(jié)論:
通過實現(xiàn)HandlerInterceptor 接口自定義攔截器AuthInterceptor:
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 在請求處理前執(zhí)行,可進(jìn)行鑒權(quán)邏輯
// 獲取當(dāng)前用戶信息
String currentUser = getCurrentUser(request);
// 進(jìn)行垂直越權(quán)檢測
if (!hasPermission(currentUser)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return false; // 拒絕訪問
}
return true; // 允許訪問
}
private String getCurrentUser(HttpServletRequest request) {
// 實現(xiàn)獲取當(dāng)前用戶的邏輯
}
private boolean hasPermission(String currentUser, String resourceId) {
// 實現(xiàn)垂直越權(quán)檢測邏輯
}
}
在配置類添加@Configuration注解,通過重寫addInterceptors方法,添加攔截器,并配置匹配路徑,AuthInterceptor對/admin/目錄下的所有資源都生效,也就是說這是一個垂直鑒權(quán)的措施:
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注冊攔截器
registry.addInterceptor(new AuthInterceptor(environment)).addPathPatterns("/admin/**").excludePathPatterns("/test/**");
}
同樣是前面過濾器Filter中訪問/user/add接口的例子,當(dāng)攔截器AuthInterceptor生效時,再次使用對應(yīng)的方式請求會返回403 Status,說明不存在對應(yīng)的身份無法訪問:
wKg0C2UZbkaAU4iAACV0MEi96U271.png
使用攔截器preHandle進(jìn)行鑒權(quán)時,對應(yīng)的解析邏輯會在Controller方法執(zhí)行之前執(zhí)行。與Controller的參數(shù)、Content-Type發(fā)送的數(shù)據(jù)類型無關(guān)。 主要是檢查請求是否具備足夠的權(quán)限訪問某個資源,而不是檢查請求的具體內(nèi)容。
但是要注意一點(diǎn),跟過濾器Filter不同的是,類似上面的案例,當(dāng)攔截器AuthInterceptor生效時,使用GET方式請求/admin/user/add(POST請求接口)接口并不會返回403 Stauts:
wKg0C2UZbm6AcBthAACWJOYgr7I431.png
這是因為在RequestMappingHandlerMapping的getHandler方法中通過getHandlerInternal獲取handler時,會對請求匹配對應(yīng)的handler,其中一處就是請求Method的匹配,若資源不存在的話,也沒必要進(jìn)一步處理了:
wKg0C2UZboeAWJgYAACEJMUzF7g096.png
2.3 切面
切面是Spring的AOP(面向切面編程)組件,用于定義橫切關(guān)注點(diǎn)。雖然它們通常用于橫切關(guān)注點(diǎn)的日志記錄、性能監(jiān)控等,但也可以用于鑒權(quán)。你可以創(chuàng)建自定義切面,它們可以攔截方法調(diào)用,以執(zhí)行鑒權(quán)邏輯。
相比其他措施, 切面的執(zhí)行順序十分靈活,可以通過配置進(jìn)行管理,它們可以被指定在其他組件之前或之后執(zhí)行。
最常見的是通過注解方式來實現(xiàn),主要包含以下解釋:
- @Aspect: 指定切面類(可以通過檢索@Aspect快速定位切面類)
- @Pointcut:定義了相應(yīng)的 Advice(具體要做的操作)觸發(fā)的地方。一般是通過通配符、正則表達(dá)式等方式。
例如如下的案例,代碼中只要被@Auth注解標(biāo)記的方法均會執(zhí)行當(dāng)前切面定義的內(nèi)容:
@Pointcut("@annotation(Auth)")
public void requirePermissionAuth() {
- 通知(Advice)類型:定義要執(zhí)行的方法,安全中一般是鑒權(quán)
@Before:在目標(biāo)方法執(zhí)行之前,執(zhí)行注解標(biāo)記的內(nèi)容
@After:在目標(biāo)方法執(zhí)行之后,執(zhí)行注解標(biāo)記的內(nèi)容
@AfterReturning:在目標(biāo)方法返回后,執(zhí)行注解標(biāo)記的內(nèi)容
@AfterThrowing:在目標(biāo)方法拋出異常后,執(zhí)行注解標(biāo)記的內(nèi)容
@Around:在目標(biāo)方法執(zhí)行前后,分別執(zhí)行對應(yīng)的內(nèi)容
在 權(quán)限校驗中,比較常用的是@Before和@Around。例如@Around,在判斷權(quán)限之后選擇對應(yīng)的函數(shù)是否執(zhí)行。如果權(quán)限滿足,那么執(zhí)行函數(shù),如果不滿足直接拋出權(quán)限不足的提示。
相比Filter和Interceptor, 切面在方法級別執(zhí)行,并且可以拿到Controller方法的參數(shù)進(jìn)行操作。
2.4 在 Service 層實施鑒權(quán)
除了上面的方式以外,如果對應(yīng)鑒權(quán)規(guī)則是基于業(yè)務(wù)邏輯的,一般還會在 Service 層實現(xiàn)鑒權(quán)。 Service 層通常包含業(yè)務(wù)邏輯和數(shù)據(jù)訪問,可以更好地控制鑒權(quán)邏輯。與路徑級別的鑒權(quán)不同,在 Service 層實施鑒權(quán)可以確保所有相關(guān)的業(yè)務(wù)方法都經(jīng)過相同的鑒權(quán)規(guī)則。
例如下面的例子,UserService中包含了一個hasPermissionToDeleteUser方法,該方法根據(jù)自定義的鑒權(quán)邏輯來檢查是否允許當(dāng)前用戶刪除指定的用戶。然后,在deleteUser方法中,我們檢查是否有權(quán)限刪除用戶,并根據(jù)鑒權(quán)結(jié)果來執(zhí)行刪除用戶的業(yè)務(wù)邏輯或拋出異常:
@Service
public class UserService {
public boolean hasPermissionToDeleteUser(User currentUser, User userToDelete) {
// 自定義鑒權(quán)邏輯,檢查是否允許當(dāng)前用戶刪除指定用戶
if (currentUser.isAdmin()) {
return true; // 管理員可以刪除任何用戶
} else {
return currentUser.getId().equals(userToDelete.getId()); // 用戶只能刪除自己
}
}
public User deleteUser(User currentUser, User userToDelete) {
if (hasPermissionToDeleteUser(currentUser, userToDelete)) {
// 執(zhí)行刪除用戶的業(yè)務(wù)邏輯
return userToDelete;
} else {
throw new SecurityException("沒有權(quán)限刪除用戶");
}
}
// 其他方法...
}
因為Service的調(diào)用一般都是在Controller,此時已經(jīng)完成路徑解析&匹配了,相比于前面的措施,基本上是最后才進(jìn)行解析。
三、鑒權(quán)措施的執(zhí)行順序
根據(jù)上面的分析,可以大概知道,當(dāng)一個請求到達(dá)時,執(zhí)行順序是:Filter過濾器> Interceptor攔截器> ControllerAdvice > AOP > Controller,在Controller之后,就是具體的service調(diào)用了。
wKg0C2UZatyAVJXiAAByCYwBOc979.png
四、簡單的垂直越權(quán)檢測
對于垂直越權(quán)的場景,一般情況下在系統(tǒng)設(shè)計開發(fā)時,會根據(jù)路由進(jìn)行權(quán)限角色的區(qū)分,過攔截器(Interceptor)或過濾器(Filter)之類的中間件組件來保護(hù)應(yīng)用程序。
以SpringSecurity為例,Spring Security內(nèi)部其實是通過一個過濾器鏈來實現(xiàn)認(rèn)證/鑒權(quán)等流程的。例如下面的例子,這里限制了/admin/以及/manage目錄下的接口均需要ADMIN角色才能進(jìn)行訪問:
@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();
}
}
根據(jù)前面的分析,也就是說不論/admin下的接口參數(shù)是否已知,是否有完整的請求流量,都可以通過直接請求接口的方式進(jìn)行驗證,若返回接口不是403,則可能存在垂直越權(quán)的風(fēng)險:
wKg0C2UZayiAJMayAADNctgQos390.png
對于基于Spring開發(fā)系統(tǒng),可以結(jié)合鑒權(quán)措施的執(zhí)行順序,在一定程度上解決垂直越權(quán)測試上的問題,但是對于平行越權(quán)來說,一般情況下的防護(hù)措施是基于業(yè)務(wù)邏輯的,一般會在 Service 層實現(xiàn)鑒權(quán)。這里就必須獲取到對應(yīng)的請求參數(shù)了或者完整的流量了。
本文作者:SecIN技術(shù)社區(qū), 轉(zhuǎn)載請注明來自FreeBuf.COM