1. Introduction
Spring Framework versions 5.0 to 5.0.4, 4.3 to 4.3.14, and other older versions had a directory or path traversal security vulnerability on Windows systems.
Misconfiguring the static resources allows malicious users to access the server’s file system. For instance, serving static resources using file: protocol provides illegal access to the file system on Windows.
The Spring Framework acknowledged the vulnerability and fixed it in the later releases.
Consequently, this fix guards the applications against path traversal attacks. However, with this fix, a few of the earlier URLs now throw an org.springframework.security.web.firewall.RequestRejectedException exception*.*
Finally, in this tutorial, let’s learn about org.springframework.security.web.firewall.RequestRejectedException and StrictHttpFirewall in the context of path traversal attacks.
2. Path Traversal Vulnerabilities
A path traversal or directory traversal vulnerability enables illegal access outside the web document root directory. For instance, manipulating the URL can provide unauthorized access to the files outside the document root.
Though most latest and popular webservers offset most of these attacks, the attackers can still use URL-encoding of special characters like “./”, “../” to circumvent the webserver security and gain illegal access.
Also, OWASP discusses the Path Traversal vulnerabilities and the ways to address them.
3. Spring Framework Vulnerability
Now, Let’s try to replicate this vulnerability before we learn how to fix it.
First, let’s clone the Spring Framework MVC examples. Later, let’s modify the pom.xml and replace the existing Spring Framework version with a vulnerable version.
Clone the repository:
git clone [email protected]:spring-projects/spring-mvc-showcase.git
Inside the cloned directory, edit the pom.xml to include 5.0.0.RELEASE as the Spring Framework version:
<org.springframework-version>5.0.0.RELEASE</org.springframework-version>
Next, edit the web configuration class WebMvcConfig and modify the addResourceHandlers method to map resources to a local file directory using file:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry
.addResourceHandler("/resources/**")
.addResourceLocations("file:./src/", "/resources/");
}
Later, build the artifact and run our web app:
mvn jetty:run
Now, when the server starts up, invoke the 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 is a double-encoded form of ..\ and %255c%255c is a double-encoded form of \\.
Precariously, the response will be the contents of the Windows system file system.ini.
4. Spring Security HttpFirewall Interface
The Servlet specification does not precisely define the distinction between servletPath and pathInfo. Hence, there is an inconsistency among the Servlet containers in the translation of these values.
For instance, on Tomcat 9, for the URL http://localhost:8080/api/v1/users/1, the URI /1 is intended to be a path variable.
On the other hand, the following returns /api/v1/users/1:
request.getServletPath()
However, the command below returns a null:
request.getPathInfo()
Unable to distinguish the path variables from the URI can lead to potential attacks like Path Traversal / Directory Traversal attacks. For instance, a user can exploit system files on the server by including a \\, /../, ..\ in the URL. Unfortunately, only some Servlet containers normalize these URLs.
Spring Security to the rescue. Spring Security consistently behaves across the containers and normalizes these kinds of malicious URLs utilizing a HttpFirewall interface. This interface has two implementations:
4.1. DefaultHttpFirewall
In the first place, let’s not get confused with the name of the implementation class. In other words, this is not the default HttpFirewall implementation.
The firewall tries to sanitize or normalize the URLs and standardizes the servletPath and pathInfo across the containers. Also, we can override the default HttpFirewall behavior by explicitly declaring a @Bean:
@Bean
public HttpFirewall getHttpFirewall() {
return new DefaultHttpFirewall();
}
However, StrictHttpFirewall provides a robust and secured implementation and is the recommended implementation.
4.2. StrictHttpFirewall
StrictHttpFirewall* is the default and stricter implementation of *HttpFirewall. In contrast, unlike DefaultHttpFirewall, StrictHttpFirewall rejects any un-normalized URLs providing more stringent protection. In addition, this implementation protects the application from several other attacks like Cross-Site Tracing (XST) and HTTP Verb Tampering.
Moreover, this implementation is customizable and has sensible defaults. In other words, we can disable (not recommended) a few of the features like allowing semicolons as part of the URI:
@Bean
public HttpFirewall getHttpFirewall() {
StrictHttpFirewall strictHttpFirewall = new StrictHttpFirewall();
strictHttpFirewall.setAllowSemicolon(true);
return strictHttpFirewall;
}
In short, StrictHttpFirewall rejects suspicious requests with a org.springframework.security.web.firewall.RequestRejectedException.
Finally, let’s develop a User Management application with CRUD operations on Users using Spring REST and Spring Security, and see StrictHttpFirewall in action.
5. Dependencies
Let’s declare Spring Security and Spring Web dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.1.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.5</version>
</dependency>
6. Spring Security Configuration
Next, let’s secure our application with Basic Authentication by creating a configuration class that creates a SecurityFilterChain bean:
@Configuration
public class HttpFirewallConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry ->
authorizationManagerRequestMatcherRegistry.requestMatchers("/error").permitAll().anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
By default, Spring Security provides a default password that changes for every restart. Hence, let’s create a default username and password in the application.properties:
spring.security.user.name=user
spring.security.user.password=password
Henceforth, we’ll access our secured REST APIs using these credentials.
7. Building a Secured REST API
Now, let’s build our User Management 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()));
}
Now, let’s build and run the application:
mvn spring-boot:run
8. Testing the APIs
Now, let’s start by creating a User using cURL:
curl -i --user user:password -d @request.json -H "Content-Type: application/json"
-H "Accept: application/json" http://localhost:8080/api/v1/users
Here is a request.json:
{
"id":"1",
"username":"navuluri",
"email":"[email protected]"
}
Consequently, the response is:
HTTP/1.1 201
Location: /users/1
Content-Type: application/json
{
"code":201,
"message":"User created successfully",
"timestamp":1632808055618
}
Now, let’s configure our StrictHttpFirewall to deny requests from all the HTTP methods:
@Bean
public HttpFirewall configureFirewall() {
StrictHttpFirewall strictHttpFirewall = new StrictHttpFirewall();
strictHttpFirewall
.setAllowedHttpMethods(Collections.emptyList());
return strictHttpFirewall;
}
Next, let’s invoke the API again. Since we configured StrictHttpFirewall to restrict all the HTTP methods, this time, we get an error.
In the logs, we have this exception:
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 []
Since Spring Security v6.1.5, we can use RequestRejectedHandler to customize the HTTP Status when there is a RequestRejectedException:
@Bean
public RequestRejectedHandler requestRejectedHandler() {
return new HttpStatusRequestRejectedHandler();
}
Note that the default HTTP status code when using a HttpStatusRequestRejectedHandler is 400. However, we can customize this by passing a status code in the constructor of the HttpStatusRequestRejectedHandler class.
Now, let’s reconfigure the StrictHttpFirewall to allow *\\* in the URL and HTTP GET, POST, DELETE, and OPTIONS methods:
strictHttpFirewall.setAllowBackSlash(true);
strictHttpFirewall.setAllowedHttpMethods(Arrays.asList("GET","POST","DELETE", "OPTIONS")
Next, invoke the API:
curl -i --user user:password -d @request.json -H "Content-Type: application/json"
-H "Accept: application/json" http://localhost:8080/api<strong>\\</strong>v1/users
And here we have a response:
{
"code":201,
"message":"User created successfully",
"timestamp":1632812660569
}
Finally, let’s revert to the original strict functionality of StrictHttpFirewall by deleting the @Bean declaration.
Next, let’s try to invoke our API with suspicious URLs:
curl -i --user user:password -d @request.json -H "Content-Type: application/json"
-H "Accept: application/json" http://localhost:8080/api/v1<strong>//</strong>users
curl -i --user user:password -d @request.json -H "Content-Type: application/json"
-H "Accept: application/json" http://localhost:8080/api/v1<strong>\\</strong>users
Straightaway, all the above requests fail with error log:
org.springframework.security.web.firewall.RequestRejectedException:
The request was rejected because the URL contained a potentially malicious String "//"
9. Conclusion
This article explains Spring Security’s protection against malicious URLs that may cause the Path Traversal/Directory Traversal attacks.
DefaultHttpFirewall tries to normalize the malicious URLs. However, StrictHttpFirewall rejects the requests with a RequestRejectedException. Along with Path Traversal attacks, StrictHttpFirewall protects us from several other attacks. Hence it is highly recommended to use the StrictHttpFirewall along with its default configurations.
As always, the complete source code is available over on Github.