Spring Data REST代码审计浅析

admin 2024年3月8日01:05:23评论9 views字数 10161阅读33分52秒阅读模式

0x00 前言

Springboot + Spring MVC大大简化了Web应用的RESTful开发,而Spring Data REST更简单。Spring Data REST是建立在Data Repository之上的,它能直接把resository以HATEOAS风格暴露成Web服务,而不需要再手写Controller层。客户端可以轻松查询并调用存储库本身暴露出来的接口。

Spring Data REST代码审计浅析

0x01 请求路径组成

首先是Spring Data REST的根URL属性basePath。

默认情况下,Spring Data REST 在根 URI/处提供 REST 资源。可以通过spring.data.rest.basePath 属性进行修改该属性用于设置仓库资源路径的基本路径。

通过使用如下配置,所有的路由都会以 /api 为基础构建,包括仓库路径、实体资源路径、查询路径等:

spring.data.rest.basePath=/api

当然也可以通过通过注册RepositoryRestConfigurer(或扩展RepositoryRestConfigurerAdapter(高版本已经弃用))来自定义配置,例如下面的例子:

@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的路径组成:

@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)

  • 用于导航到实体之间的关系。
  • 例如,如果 TenantAddress 有关联关系,可能存在 /tenants/1/address 的关系路径。
  • Profile 路径

  • /profile 用于查看服务器支持的功能和约束信息。
  • 例如,/profile 提供有关 Spring Data REST 服务器配置和功能的元信息。

0x02 请求解析过程

Spring Data REST本身是一个Spring MVC的应用。以spring-data-rest-webmvc-3.7.18以及下面的Repository为例:

@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中定义):

Spring Data REST代码审计浅析

前面的流程跟SpringWeb类似,经过一系列处理后,会在getHandler方法中,按顺序循环调用HandlerMapping的getHandler方法:

Spring Data REST代码审计浅析

这里不再使用RequestMappingHandlerMapping,会使用DelegatingHandlerMapping,然后委托给RepositoryRestHandlerMapping和BasePathAwareHandlerMapping处理

Spring Data REST代码审计浅析

Spring Data REST代码审计浅析

以RepositoryRestHandlerMapping为例,从org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping#isHandlerInternal方法可以知道,主要是处理RepositoryRestController注解类:

Spring Data REST代码审计浅析

首先在RepositoryRestHandlerMapping#getHandler方法中通过getHandlerInternal获取handler构建HandlerExecutionChain并返回:

Spring Data REST代码审计浅析

getHandlerInternal方法会调用org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal从request对象中获取请求的path并根据path找到handlerMethod:

Spring Data REST代码审计浅析

这里跟SpringWeb类似,在initLookupPath方法中,主要用于初始化请求映射的路径,这里会根据是否使用PathPattern解析器来调用UrlPathHelper类进行不同程度路径的处理(具体可以参考https://forum.butian.net/share/2214) :

Spring Data REST代码审计浅析

获取到路径后,调用RepositoryRestHandlerMapping#lookupHandlerMethod方法,首先调用父类org.springframework.data.rest.webmvc.BasePathAwareHandlerMapping#lookupHandlerMethod方法进行处理:

Spring Data REST代码审计浅析

在BasePathAwareHandlerMapping#lookupHandlerMethod方法中,首先从请求头中获取Accept的值:

Spring Data REST代码审计浅析

对获取到的值进行处理,将配置中设置的默认媒体类型加入媒体类型列表中,然后调用父类的RequestMappingHandlerMapping#lookupHandlerMethod方法进行进一步处理,跟SpringWeb类似,首先直接根据路径获取对应的Mapping,获取不到的话调用addMatchingMappings遍历所有的ReuqestMappingInfo对象并进行匹配:

Spring Data REST代码审计浅析

例如当前的ReuqestMappingInfo对象如下:

Spring Data REST代码审计浅析

在addMatchingMappings方法中,遍历识别到的ReuqestMappingInfo对象并进行匹配,跟SpringWeb类似,在getMatchingCondition中会根据不同版本调用不同的解析模式来匹配,高版本会使用PathPattern来进行URL匹配(不同版本会有差异,在 2.6之前,默认使用的是AntPathMatcher进行的字符串模式匹配):

Spring Data REST代码审计浅析

在获取到对应的handlerMethod后,回到RepositoryRestHandlerMapping#lookupHandlerMethod的逻辑,如果反悔的handlerMethod为null则直接返回,否则进一步调用BaseUril#getRepositoryLookupPath方法获取repositoryLookupPath:

Spring Data REST代码审计浅析

getRepositoryLookupPath具体实现如下,这里主要是对baseUri进行处理,以得到最终的仓库查找路径,例如如果配置了spring.data.rest.base-path=api那么会剔除掉路径里的/api,这里还进行了一些额外的处理:

  • 将重复的斜杠(//)替换为单个斜杠(/)
  • 去除路径末尾的斜杠

Spring Data REST代码审计浅析

然后会调用getRepositoryBasePath方法:

Spring Data REST代码审计浅析

根据 repositoryLookupPath是否以/来获取第二个目录斜杠的索引,这是因为,如果 repositoryLookupPath以斜杠开头,第一个斜杠是路径的一部分,应该从第二个斜杠处分割,这个方法用于根据仓库查找路径提取仓库的基本路径,以便确定请求中要访问的具体仓库:

Spring Data REST代码审计浅析

在获取到RepositoryBasePath后,调用org.springframework.data.rest.core.mapping.PersistentEntitiesResourceMappings#exportsTopLevelResourceFor方法,判断与metadata的path是否匹配:

Spring Data REST代码审计浅析

这里首先使用 Pattern.quote 对匹配的字符串进行转义,然后通过正则进行匹配:

Spring Data REST代码审计浅析

若匹配失败则返回null,否则继续调用exposeEffectiveLookupPathKey方法进行处理:

Spring Data REST代码审计浅析

在exposeEffectiveLookupPathKey方法中,在获取到对应的Pattern后,例如/api/{repository}/search/{search},会把/{repository}替换成前面获取到的repositoryBasePath,然后创建路径模式解析器PathPattern,并将其设置到请求属性中,以便后续处理中使用:

Spring Data REST代码审计浅析

在获取到url 和 Handler 映射关系后,就可以根据请求的uri来找到对应的Controller和method,处理和响应请求。这里一般处理的是RepositoryRestController注解类。

上述案例最终映射到的org.springframework.data.rest.webmvc.RepositorySearchController#executeSearch:

Spring Data REST代码审计浅析

首先会调用checkExecutability处理,实际上就是匹配调用的方法,例如案例中的是findAllByIdCardContaining:

Spring Data REST代码审计浅析

这里首先会获取所有的SearchResourceMappings:

Spring Data REST代码审计浅析

Spring Data REST代码审计浅析

在获取时会对@RestResource注解进行处理,这里可以自定义访问的path:

Spring Data REST代码审计浅析

若当前的resourceMappings都是非暴露的,则会抛出异常,否则继续调用searchMapping.getMappedMethod进行匹配:

Spring Data REST代码审计浅析

在匹配前会先将对应的path封装在org.springframework.data.rest.core.Path对象中,这里cleanUp默认是true:

Spring Data REST代码审计浅析

在cleanUp方法中,会对对输入的路径字符串进行清理和规范化处理:

  • 使用 path.trim().replaceAll(" ", "") 去除路径两端的空格,并将路径中的空格替换为空字符串。
  • 截取路径,并在需要时添加斜杠

Spring Data REST代码审计浅析

然后进行匹配,匹配到对应的Method后进行返回。回到Controller的逻辑调用executeQueryMethod方法进行处理,获取对应Repository方法操作的结果:

Spring Data REST代码审计浅析

获取到result后,这里重新获取所有的SearchResourceMappings,然后调用getExportedMethodMappingForPath进行处理,这里会重新对当前path进行匹配,检查对应的mapping是否暴露且对应的path是否匹配,匹配的方式跟之前一样,也是通过正则表达式对 Pattern.quote 转义后的字符串进行匹配:

Spring Data REST代码审计浅析

最后获取对应的返回值,完成对应的响应。以上是Spring Data REST简单的search请求解析过程。大致可以总结为,通过请求的path,找到对应的Repository定义,然后通过Repository定义,使用对应的RepositoryInvoker并执行对应的方法。

2.1 与SpringWeb的区别

整体的调用过程与SpringWeb类似,都会通过DispatchServlet统一处理。但是不再使用RequestMappingHandlerMapping,会使用DelegatingHandlerMapping,然后委托给RepositoryRestHandlerMapping和BasePathAwareHandlerMapping处理。

路径处理模式也是类似的,同样的会在initLookupPath方法中初始化请求映射的路径,并且根据是否使用PathPattern解析器来调用UrlPathHelper类进行不同程度路径的处理:

Spring Data REST代码审计浅析

也就是说,类似SpringWeb中的一些变形后的URL,在Spring Data REST同样可以处理。但是实际上还是会有一些区别。以上述分析的/{repository}/search/{search}请求过程为例。

在SpringWeb中,是可以对请求路径进行URL编码并且均可以正常解析的。但是对于类似Spring Data REST中的/{repository}/search/{search}接口,{repository}并不能进行URL编码处理:

Spring Data REST代码审计浅析

根据前面的分析,在获取到RepositoryBasePath后,调用org.springframework.data.rest.core.mapping.PersistentEntitiesResourceMappings#exportsTopLevelResourceFor方法,判断与metadata的path是否匹配:

Spring Data REST代码审计浅析

这里首先使用 Pattern.quote 对匹配的字符串进行转义,然后通过正则进行匹配:

Spring Data REST代码审计浅析

这里并不会进行URL解码处理,所以返回404 status。

同理,类似/{repository}/{id}的访问也没办法解析编码后的{repository}

Spring Data REST代码审计浅析

可以看到返回了404 status:

Spring Data REST代码审计浅析

但是类似尾部额外的/,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类:

Spring Data REST代码审计浅析

通过访问 /profile 路径,AlpsController 会提供 ALPS 文档,例如下面的例子:

Spring Data REST代码审计浅析

其中具体的文档包含了关于支持的资源、属性、关系和操作的信息:

Spring Data REST代码审计浅析

在某些情况下可能存在信息泄露的风险。

3.1.1 禁用Alps

可以看到,是否启用Alps主要是通过org.springframework.data.rest.core.config.MetadataConfiguration的alpsEnabled属性来控制的:

Spring Data REST代码审计浅析

默认情况下alpsEnabled的值为true:

Spring Data REST代码审计浅析

可以通过注册RepositoryRestConfigurer(或扩展RepositoryRestConfigurerAdapter(高版本已经弃用))来自定义配置,例如下面的例子:

@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来自定义配置:

@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:

Spring Data REST代码审计浅析

3.2 暴露的端点

在Spring Data REST中可以通过如下方式将exported属性设置为false来实现接口及接口中的所有方法不对外暴露,从而限制访问。一般情况下为了防止HTTP用户调用CrudRepository的删除方法,会覆盖所有这些删除方法,并将对应的注释添加到覆盖方法中。

  • 在接口级别增加@RepositoryRestResource(exported = false)

Spring Data REST代码审计浅析

  • 在指定的方法使用@RestResource(exported = false)

Spring Data REST代码审计浅析

此外,还可以通过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:

Spring Data REST代码审计浅析

在exposeEffectiveLookupPathKey方法中,在获取到对应的Pattern后,例如/api/{repository}/search/{search},会把/{repository}替换成前面获取到的repositoryBasePath:

Spring Data REST代码审计浅析

也就是说,可以通过下面的方法来获取包含仓库路径Repository Path的请求path:

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注入问题在审计时也是需要关注的。

转载:作者:欢迎大家去关注作者

原文始发于微信公众号(星冥安全):Spring Data REST代码审计浅析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年3月8日01:05:23
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Spring Data REST代码审计浅析https://cn-sec.com/archives/2557429.html

发表评论

匿名网友 填写信息