问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
Spring Data REST代码审计浅析
渗透测试
Spring Data REST是建立在Data Repository之上的,它能直接把resository以HATEOAS风格暴露成Web服务,而不需要再手写Controller层。客户端可以轻松查询并调用存储库本身暴露出来的接口。浅析其中的代码审计技巧。
0x00 前言 ======= Springboot + Spring MVC大大简化了Web应用的RESTful开发,而Spring Data REST更简单。Spring Data REST是建立在Data Repository之上的,它能直接把resository以HATEOAS风格暴露成Web服务,而不需要再手写Controller层。客户端可以轻松查询并调用存储库本身暴露出来的接口。  0x01 请求路径组成 =========== 首先是Spring Data REST的根URL属性basePath。 默认情况下,Spring Data REST 在根 URI`/`处提供 REST 资源。可以通过`spring.data.rest.basePath` 属性进行修改该属性用于设置仓库资源路径的基本路径。 通过使用如下配置,所有的路由都会以 `/api` 为基础构建,包括仓库路径、实体资源路径、查询路径等: ```Java spring.data.rest.basePath=/api ``` 当然也可以通过通过注册RepositoryRestConfigurer(或扩展RepositoryRestConfigurerAdapter(高版本已经弃用))来自定义配置,例如下面的例子: ```Java @Configuration class CustomRestMvcConfiguration { @Bean public RepositoryRestConfigurer repositoryRestConfigurer() { return new RepositoryRestConfigurerAdapter() { @Override public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) { configuration.setBasePath("/api") } }; } } ``` 然后就是Sping Data REST生成的rest接口,在进行路由处理时会使用**DelegatingHandlerMapping,然后委托给RepositoryRestHandlerMapping和BasePathAwareHandlerMapping处理。** 主要是处理@RepositoryRestController和@BasePathAwareController对应的类: - @RepositoryRestController - RepositoryController - RepositoryEntityController - RepositoryPropertyReferenceController - RepositorySearchController - @BasePathAwareController - ProfileController - AlpsController - HalExplorer 以下面的例子为例,生成的rest接口都会由上面几个Controller进行处理,简单看看Spring Data REST的路径组成: ```Java @RepositoryRestResource(path = "tenantPath") public interface TenantRepository extends CrudRepository<Tenant, Long> { Page<Tenant> findAllByNameContaining(String name, Pageable page); Page<Tenant> findAllByIdCardContaining(String idCard, Pageable page); @RestResource(path = "mobile",rel = "mobile") Tenant findFirstByMobile(String mobile); @RestResource(exported = false) Tenant findFirstByIdCard(String idCard); } ``` - **仓库路径(Repository Path)**: - 仓库路径是仓库资源路径的子路径,表示单个仓库的根路径。 - 例如,如果有一个名为 `TenantRepository` 的仓库,其路径可能为 `/tenants`(默认情况下)或根据@RepositoryRestResource注解的配置路径为`tenantPath` - **实体资源路径(Entity Resource Path)**: - 实体资源路径是仓库路径的子路径,表示具体实体资源的路径。 - 例如,`Tenant` 实体的路径可能是 `/tenants/1` - **查询路径(Search Path)**: - 用于执行仓库中定义的查询。 - 例如,`findAllByNameContaining` 查询的路径可能是 `/tenants/search/findAllByNameContaining` ,也可以通过注解@RestResource进行定义。 - **关系路径(Association Path)**: - 用于导航到实体之间的关系。 - 例如,如果 `Tenant` 与 `Address` 有关联关系,可能存在 `/tenants/1/address` 的关系路径。 - **Profile 路径**: - `/profile` 用于查看服务器支持的功能和约束信息。 - 例如,`/profile` 提供有关 Spring Data REST 服务器配置和功能的元信息。 0x02 请求解析过程 =========== Spring Data REST本身是一个Spring MVC的应用。以spring-data-rest-webmvc-3.7.18以及下面的Repository为例: ```Java @RepositoryRestResource(path = "tenantPath") public interface TenantRepository extends CrudRepository<Tenant, Long> { Page<Tenant> findAllByIdCardContaining(String idCard, Pageable page) } ``` 当请求/tenantPath/search/findAllByIdCardContaining时,查看具体的请求解析过程: 当接收到请求后,跟SpringWeb类似,Servlet容器会调用DispatcherServlet的service方法(方法的实现在其父类FrameworkServlet中定义):  前面的流程跟SpringWeb类似,经过一系列处理后,会在getHandler方法中,按顺序循环调用HandlerMapping的getHandler方法:  这里不再使用RequestMappingHandlerMapping,会使用**DelegatingHandlerMapping,然后委托给RepositoryRestHandlerMapping和BasePathAwareHandlerMapping处理**:   以RepositoryRestHandlerMapping为例,从org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping#isHandlerInternal方法可以知道,主要是处理RepositoryRestController注解类:  首先在RepositoryRestHandlerMapping#getHandler方法中通过getHandlerInternal获取handler构建HandlerExecutionChain并返回:  getHandlerInternal方法会调用org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal从request对象中获取请求的path并根据path找到handlerMethod:  这里跟SpringWeb类似,在initLookupPath方法中,主要用于初始化请求映射的路径,这里会根据是否使用PathPattern解析器来调用**UrlPathHelper**类进行不同程度路径的处理(具体可以参考https://forum.butian.net/share/2214) :  获取到路径后,调用RepositoryRestHandlerMapping#lookupHandlerMethod方法,首先调用父类org.springframework.data.rest.webmvc.BasePathAwareHandlerMapping#lookupHandlerMethod方法进行处理:  在BasePathAwareHandlerMapping#lookupHandlerMethod方法中,首先从请求头中获取`Accept`的值:  对获取到的值进行处理,将配置中设置的默认媒体类型加入媒体类型列表中,然后调用父类的RequestMappingHandlerMapping#lookupHandlerMethod方法进行进一步处理,跟SpringWeb类似,首先直接根据路径获取对应的Mapping,获取不到的话调用addMatchingMappings遍历所有的ReuqestMappingInfo对象并进行匹配:  例如当前的ReuqestMappingInfo对象如下:  在addMatchingMappings方法中,遍历识别到的ReuqestMappingInfo对象并进行匹配,跟SpringWeb类似,在getMatchingCondition中会根据不同版本调用不同的解析模式来匹配,高版本会使用PathPattern来进行URL匹配(**不同版本会有差异,在 2.6之前,默认使用的是AntPathMatcher**进行的字符串模式匹配):  在获取到对应的handlerMethod后,回到RepositoryRestHandlerMapping#lookupHandlerMethod的逻辑,如果反悔的handlerMethod为null则直接返回,否则进一步调用BaseUril#getRepositoryLookupPath方法获取repositoryLookupPath:  getRepositoryLookupPath具体实现如下,这里主要是对baseUri进行处理,以得到最终的仓库查找路径,例如如果配置了`spring.data.rest.base-path=api`那么会剔除掉路径里的`/api`,这里还进行了一些额外的处理: - 将重复的斜杠(//)替换为单个斜杠(/) - 去除路径末尾的斜杠  然后会调用getRepositoryBasePath方法:  根据 repositoryLookupPath是否以`/`来获取第二个目录斜杠的索引,这是因为,如果 repositoryLookupPath以斜杠开头,第一个斜杠是路径的一部分,应该从第二个斜杠处分割,这个方法用于根据仓库查找路径提取仓库的基本路径,以便确定请求中要访问的具体仓库:  在获取到RepositoryBasePath后,调用org.springframework.data.rest.core.mapping.PersistentEntitiesResourceMappings#exportsTopLevelResourceFor方法,判断与metadata的path是否匹配:  这里首先使用 Pattern.quote 对匹配的字符串进行转义,然后通过正则进行匹配:  若匹配失败则返回null,否则继续调用exposeEffectiveLookupPathKey方法进行处理:  在exposeEffectiveLookupPathKey方法中,在获取到对应的Pattern后,例如`/api/{repository}/search/{search}`,会把`/{repository}`替换成前面获取到的repositoryBasePath,然后创建路径模式解析器PathPattern,并将其设置到请求属性中,以便后续处理中使用:  在获取到url 和 Handler 映射关系后,就可以根据请求的uri来找到对应的Controller和method,处理和响应请求。这里一般处理的是RepositoryRestController注解类。 上述案例最终映射到的org.springframework.data.rest.webmvc.RepositorySearchController#executeSearch:  首先会调用checkExecutability处理,实际上就是匹配调用的方法,例如案例中的是findAllByIdCardContaining:  这里首先会获取所有的SearchResourceMappings:   在获取时会对@RestResource注解进行处理,这里可以自定义访问的path:  若当前的resourceMappings都是非暴露的,则会抛出异常,否则继续调用searchMapping.getMappedMethod进行匹配:  在匹配前会先将对应的path封装在org.springframework.data.rest.core.Path对象中,这里cleanUp默认是true:  在cleanUp方法中,会对对输入的路径字符串进行清理和规范化处理: - 使用 `path.trim().replaceAll(" ", "")` 去除路径两端的空格,并将路径中的空格替换为空字符串。 - 截取路径,并在需要时添加斜杠  然后进行匹配,匹配到对应的Method后进行返回。回到Controller的逻辑调用executeQueryMethod方法进行处理,获取对应Repository方法操作的结果:  获取到result后,这里重新获取所有的SearchResourceMappings,然后调用getExportedMethodMappingForPath进行处理,这里会重新对当前path进行匹配,检查对应的mapping是否暴露且对应的path是否匹配,匹配的方式跟之前一样,也是通过正则表达式对 Pattern.quote 转义后的字符串进行匹配:  最后获取对应的返回值,完成对应的响应。以上是Spring Data REST简单的search请求解析过程。大致可以总结为,通过请求的path,找到对应的Repository定义,然后通过Repository定义,使用对应的RepositoryInvoker并执行对应的方法。 2.1 与SpringWeb的区别 ----------------- 整体的调用过程与SpringWeb类似,都会通过DispatchServlet统一处理。但是不再使用RequestMappingHandlerMapping,会使用**DelegatingHandlerMapping,然后委托给RepositoryRestHandlerMapping和BasePathAwareHandlerMapping处理。** 路径处理模式也是类似的,同样的会在initLookupPath方法中初始化请求映射的路径,并且根据是否使用PathPattern解析器来调用**UrlPathHelper**类进行不同程度路径的处理:  也就是说,类似SpringWeb中的一些变形后的URL,在Spring Data REST同样可以处理。但是实际上还是会有一些区别。以上述分析的`/{repository}/search/{search}`请求过程为例。 在SpringWeb中,是可以对请求路径进行URL编码并且均可以正常解析的。但是对于类似Spring Data REST中的`/{repository}/search/{search}`接口,`{repository}`并不能进行URL编码处理:  根据前面的分析,在获取到RepositoryBasePath后,调用org.springframework.data.rest.core.mapping.PersistentEntitiesResourceMappings#exportsTopLevelResourceFor方法,判断与metadata的path是否匹配:  这里首先使用 Pattern.quote 对匹配的字符串进行转义,然后通过正则进行匹配:  这里并不会进行URL解码处理,所以返回404 status。 同理,类似`/{repository}/{id}`的访问也没办法解析编码后的`{repository}`:  可以看到返回了404 status:  但是类似尾部额外的`/`,getRepositoryLookupPath时会进行额外的剔除,所以跟SpringWeb一样,在匹配时也会支持尾部额外的`/`,同样可以正常解析。 0x03 潜在的安全风险 ============ 3.1 ALPS 文档信息泄漏 --------------- ALPS 是一种描述RESTful服务中资源和操作的元数据格式。在SpringDataRest中主要是为每个导出的存储库提供一个ALPS文档。它包含有关RESTful转换以及每个存储库的属性的信息(例如服务支持的资源、属性、关系以及相关操作的 ALPS 元数据)。 `org.springframework.data.rest.webmvc.alps.AlpsController` 是 Spring Data REST 中负责处理 Application-Level Profile Semantics (ALPS) 的Controller类:  通过访问 `/profile` 路径,`AlpsController` 会提供 ALPS 文档,例如下面的例子:  其中具体的文档包含了关于支持的资源、属性、关系和操作的信息:  在某些情况下可能存在信息泄露的风险。 ### 3.1.1 禁用Alps 可以看到,是否启用Alps主要是通过org.springframework.data.rest.core.config.MetadataConfiguration的alpsEnabled属性来控制的:  默认情况下alpsEnabled的值为true:  可以通过注册RepositoryRestConfigurer(或扩展RepositoryRestConfigurerAdapter(高版本已经弃用))来自定义配置,例如下面的例子: ```Java @Configuration public class CustomRestMvcConfiguration { @Bean public RepositoryRestConfigurer repositoryRestConfigurer() { return new RepositoryRestConfigurerAdapter() { @Override public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) { MetadataConfiguration metadataConfiguration = config.getMetadataConfiguration(); metadataConfiguration.setAlpsEnabled(false); } }; } } ``` 高版本通过直接实现RepositoryRestConfigurer来自定义配置: ```Java @Configuration public class CustomRestMvcConfiguration { @Bean public RepositoryRestConfigurer repositoryRestConfigurer() { return new RepositoryRestConfigurer() { @Override public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) { MetadataConfiguration metadataConfiguration = config.getMetadataConfiguration(); metadataConfiguration.setAlpsEnabled(false); } }; } } ``` 同样是上面的例子,通过对应的配置设置alpsEnabled的值为false后,尝试访问相关的ALPS 文档会返回404:  3.2 暴露的端点 --------- 在Spring Data REST中可以通过如下方式将exported属性设置为false来实现接口及接口中的所有方法不对外暴露,从而限制访问。一般情况下为了防止HTTP用户调用CrudRepository的删除方法,会覆盖所有这些删除方法,并将对应的注释添加到覆盖方法中。 - 在接口级别增加@RepositoryRestResource(exported = false)  - 在指定的方法使用@RestResource(exported = false)  此外,还可以通过SpringSecurity对相关的请求路径进行防护。网上很多的案例都是使用AntPathRequestMatcher基于Ant风格模式进行匹配的。实际上这里会存在解析差异的问题。实际上应该使用MvcRequestMatcher,在匹配时会更严谨。 如果通过自定义filter基于请求Path进行权限控制的话,这里跟SpringWeb是类似的,同样也存在解析差异导致的绕过风险。在审计过程中需要额外注意。 ### 3.2.1 获取请求路径的方式 之前简单分析了SpringWeb中获取当前请求路径的方式,具体可以参考https://forum.butian.net/share/2606。 HandlerMapping 是 Spring Framework 中用于处理请求映射的核心接口之一。它定义了一种策略,用于确定请求应该由哪个处理器(Handler)来处理。HandlerMapping 接口提供了一组方法,用于获取与请求相关的处理器。BEST\_MATCHING\_PATTERN\_ATTRIBUTE属性在处理请求时,Spring会尝试找到最适合处理请求的Controller。该属性存储了在这个过程中找到的最佳匹配的Controller。 在Spring Data Rest中也存在类似的属性`RepositoryRestHandlerMapping.`*`BEST_MATCHING_PATTERN_ATTRIBUTE`*。但是通过类似`request.getAttribute(RepositoryRestHandlerMapping.`*`BEST_MATCHING_PATTERN_ATTRIBUTE`*`);`获取的path某些时候并不能满足对应的鉴权需求。例如请求RepositoryEntityController时获取到的路径为`/{repository}/{id}`。缺少了具体的仓库路径Repository Path。 根据前面的分析,可以通过`EFFECTIVE_REPOSITORY_RESOURCE_LOOKUP_PATH`属性获取包含仓库路径Repository Path的请求path:  在exposeEffectiveLookupPathKey方法中,在获取到对应的Pattern后,例如`/api/{repository}/search/{search}`,会把`/{repository}`替换成前面获取到的repositoryBasePath:  也就是说,可以通过下面的方法来获取包含仓库路径Repository Path的请求path: ```Java PathPattern pathPattern = (PathPattern) request.getAttribute("org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping.EFFECTIVE_REPOSITORY_RESOURCE_LOOKUP_PATH"); String requestPath = pathPattern.getPatternString(); ``` 以RepositorySearchController调用为例,尝试调用前面的findFirstByMobile方法,此时获取到的请求路径为`/tenantPath/search/{search}`。 3.3 敏感数据暴露 ---------- 默认情况下,Spring Data REST 可能会暴露实体类的所有字段,包括敏感信息。 - 避免在响应中暴露敏感信息。 - 使用投影来控制响应中返回的数据。 具体使用可以参考https://docs.spring.io/spring-data/rest/docs/current-SNAPSHOT/reference/html/#projections-excerpts 3.4 Denial of Service --------------------- Spring Data REST是建立在Data Repository之上的,可能通过执行大量数据查询来导致服务器负载过高,引发拒绝服务攻击。对于查询类的接口,需要限制查询端点的返回结果数量,并配置合适的分页和排序。 3.5 其他 ------ 除此之外,本身的sql注入问题在审计时也是需要关注的。
发表于 2024-02-20 09:00:00
阅读 ( 7445 )
分类:
代码审计
1 推荐
收藏
0 条评论
请先
登录
后评论
tkswifty
64 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!