Why is this an issue?
Applications behave as filesystem oracles when they disclose to attackers if resources from the filesystem exist or not.
A user with malicious intent would inject specially crafted values, such as ../
, to change the initially intended path. The resulting
path would resolve to a location somewhere in the filesystem which the user should not normally have access to.
What is the potential impact?
An attacker exploiting a filesystem oracle vulnerability can determine if a file exists or not.
The files that can be affected are limited by the permission of the process that runs the application. Worst case scenario: the process runs with
elevated privileges, and therefore any file can be affected.
Below are some real-world scenarios that illustrate some impacts of an attacker exploiting the vulnerability.
Information gathering
The vulnerability is exploited to gather information about the host system. The filesystem oracle can help identify user accounts, running
services, or the exact version of installed software.
How to fix it in Java SE
Code examples
The following code is vulnerable to a file system oracle as it allows testing the existence of a file anywhere on the file system.
Noncompliant code example
import java.io.File;
@Controller
public class ExampleController
{
static private String targetDirectory = "/path/to/target/directory/";
@GetMapping(value = "/exists")
public void delete(@RequestParam("filename") String filename) throws IOException {
File file = new File(targetDirectory + filename);
if (!file.exists()) { // Noncompliant
throw new IOException("File does not exists in the target directory");
}
}
}
Compliant solution
import java.io.File;
@Controller
public class ExampleController
{
static private String targetDirectory = "/path/to/target/directory/";
@GetMapping(value = "/exists")
public void delete(@RequestParam("filename") String filename) throws IOException {
File file = new File(targetDirectory + filename);
String canonicalDestinationPath = file.getCanonicalPath();
if (!canonicalDestinationPath.startsWith(targetDirectory)) {
throw new IOException("Entry is outside of the target directory");
} else if (!file.exists()) {
throw new IOException("File does not exists in the target directory");
}
}
}
How does this work?
Canonical path validation
The universal way to avoid filesystem oracle vulnerabilities is to validate paths constructed from untrusted data:
- Ensure the target directory path ends with a forward slash to prevent partial path traversal (see the "Pitfalls" section).
- Resolve the canonical path of the file by using methods like
java.io.File.getCanonicalPath
. This will resolve relative paths or
path components like ../
and remove any ambiguity regarding the file’s location.
- Check that the canonical path is within the directory where the file should be located.
Important Note: The order of this process pattern is important. The code must follow this order exactly to be secure by
design:
-
data = transform(user_input);
-
data = normalize(data);
-
data = sanitize(data);
-
use(data);
As pointed out in this SonarSource talk, failure to follow this exact order leads to
security vulnerabilities.
Pitfalls
Partial Path Traversal
When validating untrusted paths by checking if they start with a trusted folder name, ensure the validation string contains a path
separator as the last character.
A partial path traversal vulnerability can be unintentionally introduced into the application without a
path separator as the last character of the validation strings.
For example, the following code is vulnerable to partial path injection. Note that the string targetDirectory
does not end with a path
separator:
static private String targetDirectory = "/Users/John";
@GetMapping(value = "/endpoint")
public void endpoint(@RequestParam("folder") fileName) throws IOException {
String canonicalizedFileName = fileName.getCanonicalPath();
if (!canonicalizedFileName.startsWith(targetDirectory)) {
throw new IOException("Entry is outside of the target directory");
}
}
This check can be bypassed if other directories start with John
. For instance, "/Users/Johnny".startsWith("/Users/John")
returns true
. Thus, for validation, "/Users/John"
should actually be "/Users/John/"
.
Warning: Some functions, such as getCanonicalPath
, remove the terminating path separator in their return value.
The validation code should be tested to ensure that it cannot be impacted by this issue.
Here is a real-life example of this vulnerability.
Do not use java.nio.file.Path.resolve as a validator
As specified in the official documentation, if the given parameter
is an absolute path, the base object from which the method is called is discarded and is not included in the resulting string.
This means that including untrusted data in the parameter and using the resulting string for file operations may lead to a path traversal
vulnerability.
Resources
Standards