Spring Security 中的 RequestRejectedException

1、简介

Spring 5.0 至 5.0.4、4.3 至 4.3.14 以及其他旧版本在 Windows 系统上存在目录或路径遍历安全漏洞。

静态资源配置错误会导致恶意用户访问服务器的文件系统。例如,在 Windows 上使用 file: 协议配置静态资源,可能导致用户非法访问文件系统。

Spring 承认存在该 漏洞,并在后续版本中对其进行了修复。

此修复可防止应用遭受路径遍历攻击。不过,在修复后,一些之前的 URL 现在会抛出 org.springframework.security.web.firewall.RequestRejectedException 异常。

本文先带你了解什么是 “路径遍历攻击”,在这个知识背景下再带你了解 org.springframework.security.web.firewall.RequestRejectedExceptionStrictHttpFirewall 的相关知识。

2、路径遍历漏洞

路径遍历或目录遍历漏洞可非法访问 Web 文档根目录以外的内容。例如,篡改 URL 可对文档根目录以外的文件进行未经授权的访问。

虽然大多数最新和流行的 Web 服务器都能抵消大部分攻击,但攻击者仍可使用特殊字符(如 ./../)的 URL 编码来规避 Web 服务器的安全设置并获取非法访问权限。

OWASP 介绍了路径遍历漏洞和解决方法。

3、Spring 的漏洞

先尝试复现这个漏洞,然后再介绍如何进行修复。

首先,克隆 Spring Framework MVC 示例。然后,修改 pom.xml,用一个易受攻击的版本替换现有的 Spring Framework 版本。

克隆仓库:

git clone git@github.com:spring-projects/spring-mvc-showcase.git

在克隆目录中,编辑 pom.xml,修改 Spring Framework 的版本为 5.0.0.RELEASE

<org.springframework-version>5.0.0.RELEASE</org.springframework-version>

接下来,编辑 Web 配置类 WebMvcConfig,修改 addResourceHandlers 方法,使用 file: 将资源映射到本地文件目录:

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry
      .addResourceHandler("/resources/**")
      .addResourceLocations("file:./src/", "/resources/");
}

之后,构建并运行 web 应用:

mvn jetty:run

服务器启动后,调用如下 URL:

curl 'http://localhost:8080/spring-mvc-showcase/resources/%255c%255c%252e%252e%255c/%252e%252e%255c/%252e%252e%255c/%252e%252e%255c/%252e%252e%255c/windows/system.ini'

%252e%252e%255c..\ 的二次编码,而 %255c%255c\\. 的二次编码。

准确地说,响应的是 Windows 系统文件 system.ini 的内容。

4、Spring Security HttpFirewall 接口

Servlet 规范 没有精确定义 servletPathpathInfo 之间的区别。因此,Servlet 容器在转换这些值时存在不一致的情况。

例如,在 Tomcat 9 上,对于 URL http://localhost:8080/api/v1/users/1 而言,URI /1 就是一个路径变量。

而下面的命令则返回 /api/v1/users/1

request.getServletPath()

但是,下面的命令返回 null

request.getPathInfo()

无法从 URI 中区分路径变量会导致潜在的攻击,如路径遍历/目录遍历攻击。例如,用户可以通过在 URL 中包含 \\/../..\ 来访问服务器上的系统文件。遗憾的是,只有某些 Servlet 容器会将这些 URL 规范化。

Spring Security 提供了帮助。Spring Security 可在所有容器中保持一致的行为,并利用 HttpFirewall 接口对这类恶意 URL 进行规范化处理。该接口有两种实现方式:

4.1、DefaultHttpFirewall

首先,不要被实现类的名称所迷惑。换句话说,这不是默认使用的 HttpFirewall 实现。

它会尝试对 URL 进行脱敏或规范化处理,并在各容器中统一 servletPathpathInfo

可以通过明确声明 @Bean 来覆盖默认的 HttpFirewall 行为:

@Bean
public HttpFirewall getHttpFirewall() {
    return new DefaultHttpFirewall();
}

但是不建议覆盖默认的 StrictHttpFirewall,因为它提供了一个强大且安全的实现,且是推荐的实现。

4.2、StrictHttpFirewall

StrictHttpFirewallHttpFirewall 的默认和更严格的实现。与 DefaultHttpFirewall 不同的是,StrictHttpFirewall 会拒绝任何未规范化的 URL,从而提供更严格的保护。此外,该实现还能保护应用免受其他几种攻击,如 跨站跟踪(XST)和 HTTP Verb Tampering(动词篡改)

该实现是可定制的,并有合理的默认值。换句话说,可以禁用(不建议)一些功能,比如允许分号作为 URI 的一部分:

@Bean
public HttpFirewall getHttpFirewall() {
    StrictHttpFirewall strictHttpFirewall = new StrictHttpFirewall();
    strictHttpFirewall.setAllowSemicolon(true);
    return strictHttpFirewall;
}

简而言之,StrictHttpFirewall 会抛出 org.springframework.security.web.firewall.RequestRejectedException 异常来拒绝可疑请求。

最后,让我们使用 Spring REST 和 Spring Security 开发一个用户管理应用,对用户进行 CRUD 操作,看看 StrictHttpFirewall 的实际用法。

5、依赖

创建新的 Spring Boot(2.5.4)项目,添加 Spring SecuritySpring Web 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.5.4</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.5.4</version>
</dependency>

6、Spring Security 配置

接下来,通过配置类来创建 SecurityFilterChain Bean,使用 Basic Authentication 来保护应用:

@Configuration
public class HttpFirewallConfiguration {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf()
            .disable()
            .authorizeRequests()
            .antMatchers("/error")
            .permitAll()
            .anyRequest()
            .authenticated()
            .and()
            .httpBasic();
        return http.build();
    }
}

默认情况下,Spring Security 提供了一个默认密码,但每次重启都会更改。

因此,可以在 application.properties 中创建默认用户名和密码:

spring.security.user.name=user
spring.security.user.password=password

接下来,我们将使用这个凭证访问受保护的 REST API。

7、构建受保护的 REST API

构建用户管理 REST API:

@PostMapping
public ResponseEntity<Response> createUser(@RequestBody User user) {
    userService.saveUser(user);
    Response response = new Response()
      .withTimestamp(System.currentTimeMillis())
      .withCode(HttpStatus.CREATED.value())
      .withMessage("User created successfully");
    URI location = URI.create("/users/" + user.getId());
    return ResponseEntity.created(location).body(response);
}
 
@DeleteMapping("/{userId}")
public ResponseEntity<Response> deleteUser(@PathVariable("userId") String userId) {
    userService.deleteUser(userId);
    return ResponseEntity.ok(new Response(200,
      "The user has been deleted successfully", System.currentTimeMillis()));
}

构建并运行应用:

mvn spring-boot:run

8、测试 API

现在,使用 cURL 创建一个用户:

curl -i --user user:password -d @request.json -H "Content-Type: application/json" 
     -H "Accept: application/json" http://localhost:8080/api/v1/users

request.json 如下:

{
    "id":"1",
    "username":"navuluri",
    "email":"bhaskara.navuluri@mail.com"
}

响应如下:

HTTP/1.1 201
Location: /users/1
Content-Type: application/json
{
  "code":201,
  "message":"User created successfully",
  "timestamp":1632808055618
}

现在,配置 StrictHttpFirewall,拒绝所有 HTTP 方法的请求:

@Bean
public HttpFirewall configureFirewall() {
    StrictHttpFirewall strictHttpFirewall = new StrictHttpFirewall();
    strictHttpFirewall
      .setAllowedHttpMethods(Collections.emptyList());
    return strictHttpFirewall;
}

再次调用 API。由于配置了 StrictHttpFirewall 来限制所有 HTTP 方法,所以这次响应了异常信息。

在日志中可以看到这个异常详情:

org.springframework.security.web.firewall.RequestRejectedException: 
The request was rejected because the HTTP method "POST" was not included
  within the list of allowed HTTP methods []

从 Spring Security v5.4 起,当发生 RequestRejectedException 异常时,可以使用 RequestRejectedHandler 来自定义 HTTP 状态码:

@Bean
public RequestRejectedHandler requestRejectedHandler() {
   return new HttpStatusRequestRejectedHandler();
}

注意,使用 HttpStatusRequestRejectedHandler 时的默认 HTTP 状态码是 400。不过,可以通过在 HttpStatusRequestRejectedHandler 类的构造函数中传递状态码来自定义状态码。

现在,重新配置 StrictHttpFirewall,允许在 URL 中使用 \\ 和允许 HTTP GETPOSTDELETEOPTIONS 方法:

strictHttpFirewall.setAllowBackSlash(true);
strictHttpFirewall.setAllowedHttpMethods(Arrays.asList("GET","POST","DELETE", "OPTIONS"))

接着,调用 API:

curl -i --user user:password -d @request.json -H "Content-Type: application/json" 
     -H "Accept: application/json" http://localhost:8080/api\\v1/users

响应如下:

{
  "code":201,
  "message":"User created successfully",
  "timestamp":1632812660569
}

最后,删除 @Bean 声明,恢复 StrictHttpFirewall 最初的严格功能。

接下来,尝试用可疑的 URL 调用 API:

curl -i --user user:password -d @request.json -H "Content-Type: application/json" 
      -H "Accept: application/json" http://localhost:8080/api/v1//users
curl -i --user user:password -d @request.json -H "Content-Type: application/json" 
      -H "Accept: application/json" http://localhost:8080/api/v1\\users

上述所有请求会失败,错误日志如下:

org.springframework.security.web.firewall.RequestRejectedException: 
The request was rejected because the URL contained a potentially malicious String "//"

9、总结

本文介绍了 “路径遍历/目录遍历” 漏洞,以及如何使用 Spring Security 来避免这种攻击。

总的来说,DefaultHttpFirewall 尝试对恶意的 URL 进行规范化处理。而 StrictHttpFirewall 则会通过 RequestRejectedException 异常拒绝请求。除了路径遍历攻击,StrictHttpFirewall 还可以保护应用免受其他多种攻击。因此,强烈建议使用 StrictHttpFirewall


Ref:https://www.baeldung.com/spring-security-request-rejected-exception