Five File Write Vulnerabilities in NPM resulting in RCE While Installing Packages ($15000 USD)

Original Post:
5 RCEs in npm for $15,000
We discussed this vulnerability during Episode 85 on 27 September 2021

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.