Five File Write Vulnerabilities in NPM resulting in RCE While Installing Packages ($15000 USD)
These are five issues that enabled file writes outside of the expected directory when NPM was unpacking an archive using the node-tar
library.
First, node-tar
would try to guarentee that archives wouldn’t write outside of the expected directory. It did this by checking if the target path was an absolute path, if so it stripped the absolute path root off and proceeded. The problem was that stripping this path could still leave you with an absolute path. ///some/path
for example would become //some/path
, //some/path
would not work because that is a root path on windows.
Patching this issue resulted in a change in parsing the root and using an iterative solution that will continue stripping paths until the path is no longer absolute.
The second issue as reported is kinda two issues. First, when calling something like fs.writeFileSync
to write a file it will attempt to resolve()
the arguments given. This resolution is a bit more forgiving regarding absolute paths than the node-tar
parser was and also has a bit of a gotcha we’ve seen before with path joining. If you try to join one path to an absolute path, the absolute path takes precedence. This means that providing a filename
that resolve
thinks is an absolute path will overwrite the overrule the provided directory to write to. D:different/root
will write to D:\different\root
.
The second half of that bug was in node-tar
’s attempt to prevent relative paths by stripping ..
. The problem was that it would look for ..
to exist between path separators or at the start/end of the path. c:../example
would not get stripping, but when used with a resolve
would be treated like a relative path.
Next Up are three issues had to deal with the handling of symlinks and hardlinks in an archive. It will attempt to ensure that the folders it iterates into are not symlinks. As an optimization it would also maintain a cache of directories created, if it created the directory it wouldn’t check if it was a symlink.
The first problem with this cache was that if a directory was overwritten by a symlink (or anything really) the cache entry wouldn’t be removed. So a symbolic link that had the same name as the folder would bypass the check. The path was to remove the cache entry when its changed.
Once this was believed to be patched, the bug still remained due to a difference in how paths were normalized when being added to the cache vs when being removed. When creating the directories it would split the path on \
and /
and rejoin the path using /
. When removing cache entries its looking it up path.win32.resolve
which normalizes the path using the system path separator. So the directory cache might contain C:\packages\here\example-package/example-dir
while the deletion will be looking for C:\packages\here\example-package\example-dir
. A similar issue existed on Unix filesystem, where a path name a\\x
will look for a
and a/x
but not the actual file a\\x
.
This was patched by moving all the cache code into using helpers that do the normalization. Though this time there was a problem with unicode normalization, where MacOS will do NFD normalization on unicode in the path.