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 版本。
克隆仓库:
在克隆目录中,编辑
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 应用:
服务器启动后,调用如下 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
:
但是,下面的命令返回
null
:
无法从 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
的实际用法。
创建新的 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()));
构建并运行应用:
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"
:
"[email protected]"
响应如下:
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
上述所有请求会失败,错误日志如下: