Protecting Node.js Applications from Zip Slip

Zip Slip is a recently coined phrase for a variant of the classical Path Traversal attack. In order to exploit this attack, one creates a compressed archive containing files which include .. in their directory name. Then, when unpatched libraries attempt to extract these files, the extracted files can be written to unintended
locations.

As an example, if an attacker uploads multiple *.zip files to a service, each containing different permutations of ../etc/passwd../../etc/passwd, etc., and assuming the process has write privileges to /etc/passwd, then the process which extracts the file would overwrite this important OS file. This can then leave the OS in a dangerous state (e.g., unable to boot or with login credentials known by a third party).

Some more examples of what an attacker could do includes uploading a file which overwrites the actual server.js application, replace an important system binary with a malicious one, add a configuration file which allows for remote access to a service, etc.

Here’s an example of an application vulnerable to Zip Slip:

const fs = require('fs');
const unzipper = require('unzipper'); // unzipper <= 0.8.2
fs
.createReadStream('/tmp/user-upload.zip')
.pipe(unzipper.Extract({
path: '/tmp/target'
}));

(Note that this depends on an unpatched version of the unzipper library. This library was recently updated in v0.8.3 to fix exactly this vulnerability.) In this example we have a zip file located at /tmp/user-upload.zip, and are attempting to extract the contents to /tmp/target. With the vulnerable library installed, an attacker can upload a specially crafted *.zip file and this will result in creating files outside of the intended /tmp/target destination. So if the user-upload.zip file contains an entry for ../../etc/passwd, it will write to a file at /tmp/target/../../etc/passwd, which resolves to /etc/passwd.

This is where Intrinsic comes in. Intrinsic is a module for Node.js applications which protects applications against vulnerabilities that no one has discovered yet (If you’d like to use Intrinsic to protect your app, contact us at sales@intrinsic.com). Instead of reacting to new vulnerability reports, Intrinsic is preventive. It lets you easily apply the principle of least privilege to make sure that your app is restricted to do only what it needs to do, and nothing more. Intrinsic uses a whitelist-based approach, so the only filesystem operations the application can perform are specified by you. Using Intrinsic’s simple JavaScript DSL, here is what the policies might look like for our above sample application:

policy.fs.allowWrite('/tmp/user-upload.zip');
policy.fs.allowRead('/tmp/user-upload.zip');
policy.fs.allowWrite('/tmp/extract/**');

Assuming these policies were set, and that a malicious zip file containing path traversal filenames were uploaded, the extraction operation would be denied and the following message (along with a full stack trace) would be logged in the server output:

Unhandled rejection FsPolicyViolation: POLICY_VIOLATION Can't create directory at /etc

Intrinsic can then be used to actively prevent such attacks, without any knowledge of which packages contain vulnerabilities.

Intrinsic has many more policies than just filesystem reads and writes. It also supports policies based on HTTP requests which take into account the method and path, it supports generic networking policies based on hosts and ports, it supports child process spawning, etc.


This article was written by me, Thomas Hunter II. I work at a company called Intrinsic where we specialize in writing software for securing Node.js applications. We currently have a product called Intrinsic for Lambda which follows the Least Privilege model for securing applications. Our product proactively protects Node.js applications from attackers, and is surprisingly easy to implement. If you are looking for a way to secure your Node.js applications, give us a shout at hello@intrinsic.com.