Why is this an issue?
Zip slip is a special case of path injection. It occurs when an application uses the name of an archive entry to construct a file path and access
this file without validating its path first.
This rule will consider all archives untrusted, assuming they have been created outside the application file system.
A user with malicious intent would inject specially crafted values, such as ../
, in the archive entry name to change the initial
intended path. The resulting path would resolve somewhere in the filesystem where the user should not normally have access.
What is the potential impact?
A web application is vulnerable to Zip Slip and an attacker is able to exploit it by submitting an archive he controls.
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
root privileges on Linux, and therefore any file can be affected.
Below are some real-world scenarios that illustrate some impacts of an attacker exploiting the vulnerability.
Override arbitrary files
The application opens the archive to copy its entries to the file system. The entries' names contain path traversal payloads for existing files in
the system, which are overwritten once the entries are copied. The vulnerability is exploited to corrupt files critical for the application or
operating system to work properly.
It could result in data being lost or the application being unavailable.
How to fix it in Node.js
Code examples
The following code is vulnerable to Zip Slip as it is constructing a path using an archive entry name. This path is then used to copy a file
without being validated first. Therefore, it can be leveraged by an attacker to overwrite arbitrary files.
Noncompliant code example
const AdmZip = require("adm-zip");
const upload = require('multer');
app.get('/example', upload.single('file'), (req, res) => {
const zip = new AdmZip(req.file.buffer);
const zipEntries = zip.getEntries();
zipEntries.forEach(function (zipEntry) {
var writer = fs.createWriteStream(zipEntry.entryName); // Noncompliant
writer.write(zipEntry.getData().toString("utf8"));
});
});
Compliant solution
const AdmZip = require("adm-zip");
const upload = require('multer');
const unzipTargetDir = "/example/directory/";
app.get('/example', upload.single('file'), (req, res) => {
const zip = new AdmZip(req.file.buffer);
const zipEntries = zip.getEntries();
zipEntries.forEach(function (zipEntry) {
const canonicalPath = path.normalize(unzipTargetDir + zipEntry.entryName);
if (canonicalPath.startsWith(unzipTargetDir)) {
let writer = fs.createWriteStream(canonicalPath);
writer.write(zipEntry.getData().toString("utf8"));
}
});
});
How does this work?
The universal way to prevent Zip Slip is to validate the paths constructed from untrusted archive entry names.
The validation should be done as follow:
- Resolve the canonical path of the file by using methods like
path.join
or path.normalize
. This will resolve relative
path or path components like ../
and removes any ambiguity regarding the file’s location.
- Check that the canonical path is within the directory where the file should be located.
- Ensure the target directory path ends with a forward slash to prevent partial path traversal, for example, /base/dirmalicious
starts with /base/dir but does not start with /base/dir/.
Pitfalls
Partial Path Traversal
When validating untrusted paths by checking if they start with a trusted folder name, ensure the validation strings all contain 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 variable targetDirectory
does not end
with a path separator:
const AdmZip = require("adm-zip");
const targetDirectory = "/Users/John";
app.get('/example', (req, res) => {
const canonicalPath = path.normalize(targetDirectory + req.query.filename)
if (canonicalPath.startsWith(targetDirectory)) {
const zip = new AdmZip(canonicalPath);
const zipEntries = zip.getEntries();
zipEntries.forEach(function (zipEntry) {
var writer = fs.createWriteStream(zipEntry.entryName);
writer.write(zipEntry.getData().toString("utf8"));
});
}
});
This check can be bypassed because "/Users/Johnny".startsWith("/Users/John")
returns true
. Thus, for validation,
"/Users/John"
should actually be "/Users/John/"
.
Warning: Some functions 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.
Resources
Documentation
- snyk - Zip Slip Vulnerability
Standards