Jan 16, 2026·6 min read·71 visits
node-tar <= 7.5.2 failed to sanitize the targets of hardlinks and symlinks. If an archive contains a link pointing to an absolute path (e.g., `/etc/passwd`), node-tar would happily create it, ignoring the intended extraction directory. This leads to Arbitrary File Overwrite and potential RCE via config file manipulation.
A critical path traversal vulnerability in the ubiquitous node-tar library allows malicious archives to bypass extraction root restrictions. By manipulating hardlink and symlink targets with absolute paths, attackers can overwrite arbitrary system files or poison symbolic links, effectively turning a standard unzip operation into a weaponized file system assault.
We take extraction libraries for granted. You run npm install, you unzip a backup, you pull an artifact in your CI/CD pipeline. Under the hood, specifically in the Node.js ecosystem, node-tar is the workhorse doing the heavy lifting. It’s supposed to be a boring, reliable utility. It takes a stream of bytes and turns them into files on your disk.
However, the golden rule of archive extraction is containment. When I tell the library to extract evil.tar into ./output, I expect every single file to land inside ./output. If a file lands in /etc/, the library has failed its one job. This is the contract of the preservePaths: false setting (which is the default, secure mode).
CVE-2026-23745 is a breach of that contract. It turns out that while node-tar was very careful about where it placed files, it was completely negligent about what those files pointed to. It’s like a bouncer checking your ID at the door but ignoring the loaded bazooka slung over your shoulder.
The vulnerability stems from a fundamental misunderstanding of how Node.js handles path resolution, mixed with a lapse in input validation coverage. The issue resides specifically in how Link (hardlink) and SymbolicLink entries are handled.
In Node.js, path.resolve(from, to) is the standard way to resolve paths. However, it has a quirk that trips up developers constantly: if the second argument is an absolute path, the first argument is ignored completely.
> [!NOTE]
> path.resolve('/safe/dir', '/etc/passwd') returns /etc/passwd, NOT /safe/dir/etc/passwd.
node-tar used this logic to determine where to create a hardlink. It took the current working directory (this.cwd) and tried to resolve the entry.linkpath (the target of the link) against it. Because the developer didn't check if entry.linkpath was absolute before passing it to resolve, an attacker could craft a TAR header saying: "I am a hardlink. Please link me to /root/.ssh/authorized_keys."
For symlinks, it was even lazier. The code simply passed the raw linkpath from the TAR header directly to fs.symlink. No questions asked. If the header said symlink -> /etc/shadow, node-tar obliged.
Let's look at the crime scene in src/unpack.ts. The validation routine [CHECKPATH] was guarding the front door (entry.path), but the back door (entry.linkpath) was left wide open.
The Vulnerable Logic (Conceptual):
// Hardlink handling
if (entry.type === 'Link') {
// THE BUG: If entry.linkpath is absolute, target becomes absolute
// ignoring this.cwd entirely.
const target = path.resolve(this.cwd, entry.linkpath);
fs.link(target, entry.absolute, cb);
}
// Symlink handling
if (entry.type === 'SymbolicLink') {
// THE BUG: entry.linkpath is passed raw to the filesystem
fs.symlink(entry.linkpath, entry.absolute, cb);
}The Fix (Commit 340eb28):
The patch introduces a unified sanitizer, [STRIPABSOLUTEPATH]. This method explicitly strips out root indicators (like leading slashes or drive letters) and strictly validates that the path doesn't try to traverse upwards using ...
Crucially, the patch applies this check to both properties:
// The Fix: Validate BOTH destination and link source
if (
!this[STRIPABSOLUTEPATH](entry, 'path') ||
!this[STRIPABSOLUTEPATH](entry, 'linkpath') // <--- The critical addition
) {
return false // Abort extraction if dirty
}This forces the linkpath to be relative to the extraction root, effectively jailing the operation back to the intended directory.
To exploit this, we don't need buffer overflows or ROP chains. We just need to understand the TAR format. We can manually craft a header that lies about its link target.
Attack Scenario: The Config Overwrite
Imagine a CI/CD pipeline that extracts a user-provided tarball to lint the code. The runner creates a temporary directory, extracts the files, and then cleans up.
Link (Hardlink).temp_extraction/harmless.txt/home/runner/.bashrc (Targeting the runner's shell config)tar.x({ file: 'malicious.tar', cwd: './temp' }).node-tar sees the hardlink request. It executes link('/home/runner/.bashrc', './temp/harmless.txt').
harmless.txt and .bashrc are the same inode.harmless.txt, it instantly overwrites .bashrc.Here is the visual flow of the attack:
This allows an attacker to achieve persistence or RCE the next time a shell is spawned on that machine.
This isn't just about overwriting a text file. The implications depend heavily on who is extracting the archive and where.
.bashrc, .ssh/authorized_keys, or /etc/hosts on a build agent allows for lateral movement and supply chain injection./etc/passwd named portfolio/index.html. When the web server tries to serve index.html, it reads the password file instead.node-tar is used by npm itself (though npm usually validates package contents strictly). However, custom tooling that wraps node-tar is highly vulnerable.The CVSS 4.0 score is 8.2 (High) because while it requires local interaction (uploading a file), the complexity is low and the impact on Confidentiality and Integrity is high.
The remediation is straightforward, but urgency is required due to the ubiquity of the library.
Immediate Action:
Update node-tar to version 7.5.3 or higher immediately. Check your package-lock.json or yarn.lock because this library is likely deeply nested in your dependency tree.
npm install node-tar@latest
# Or if it is a sub-dependency
npm update node-tar --depth 99Defensive Coding:
If you are using node-tar programmatically, ensure you are NOT setting preservePaths: true unless you absolutely trust the source of the archive. That setting explicitly disables protections.
Also, consider validating the contents of archives before extraction using a stream parser to check for suspicious Link or SymbolicLink targets starting with / or containing ...
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:H/VI:L/VA:N/SC:H/SI:L/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
node-tar isaacs | <= 7.5.2 | 7.5.3 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-22 (Path Traversal) |
| CVSS 4.0 | 8.2 (High) |
| Attack Vector | Local (Archive Upload) |
| Affected Components | unpack.ts (Link/SymbolicLink handling) |
| Impact | Arbitrary File Overwrite / Symlink Poisoning |
| Exploit Status | Proof of Concept Available |
The software uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the software does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.