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.RequestRejectedException
和 StrictHttpFirewall
的相关知识。
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 规范 没有精确定义 servletPath
和 pathInfo
之间的区别。因此,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 进行脱敏或规范化处理,并在各容器中统一 servletPath
和 pathInfo
。
可以通过明确声明 @Bean
来覆盖默认的 HttpFirewall
行为:
@Bean
public HttpFirewall getHttpFirewall() {
return new DefaultHttpFirewall();
}
但是不建议覆盖默认的 StrictHttpFirewall
,因为它提供了一个强大且安全的实现,且是推荐的实现。
4.2、StrictHttpFirewall
StrictHttpFirewall
是 HttpFirewall
的默认和更严格的实现。与 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 Security 和 Spring 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 GET
、POST
、DELETE
和 OPTIONS
方法:
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