From 17df0264929514599dbcb21c6578bcdfa204b04d Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 17 Jun 2026 03:05:57 +0200 Subject: [PATCH] Confer object-derived file write only for files the object owner owns (#26032) has_access_to_file() derives file access from the objects a file is attached to (knowledge bases, workspace models). Those branches returned True for any access_type whenever the user held that permission on the object, write/delete included. Since a user can create their own KB or model and attach any file they can merely READ (KB attach and the model meta.knowledge validator both gate on read access only), a user with read access to a victim file could launder it into write/delete: attach it to an object they own, then rename, overwrite or delete it via the write-gated file routes (POST /files/{id}/rename, /data/content/update, DELETE /files/{id}). This is the residual of GHSA-vjqm-6gcc-62cr (CVE-2026-54012) left open by the read-only attach validator (CWE-863). An object now confers write/delete on a file only when the object's owner owns that file, so delegation originates from the file's own owner. Read is unchanged (RAG and shared-object reads still work), and legitimate delegation is preserved: a write grant on an object whose owner owns the attached file still confers write. Applied to all three object branches: knowledge base, file home collection, and workspace model. Co-authored-by: rexpository <30176934+rexpository@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) --- .../open_webui/utils/access_control/files.py | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/backend/open_webui/utils/access_control/files.py b/backend/open_webui/utils/access_control/files.py index 3a4871aae3..ddb6acb066 100644 --- a/backend/open_webui/utils/access_control/files.py +++ b/backend/open_webui/utils/access_control/files.py @@ -38,25 +38,33 @@ async def has_access_to_file( if file.user_id == user.id: return True - # Check if the file is associated with any knowledge bases the user has access to + # Check if the file is associated with any knowledge bases the user has access to. + # An object (knowledge base or workspace model) confers write/delete on a file only when + # the object's OWNER owns that file; otherwise a read-only file laundered into an object + # the user controls would gain write/delete on it (CWE-863). Read access is unaffected. knowledge_bases = await Knowledges.get_knowledges_by_file_id(file_id, db=db) user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} for knowledge_base in knowledge_bases: - if knowledge_base.user_id == user.id or await AccessGrants.has_access( - user_id=user.id, - resource_type='knowledge', - resource_id=knowledge_base.id, - permission=access_type, - user_group_ids=user_group_ids, - db=db, - ): + if ( + knowledge_base.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge_base.id, + permission=access_type, + user_group_ids=user_group_ids, + db=db, + ) + ) and (access_type == 'read' or knowledge_base.user_id == file.user_id): return True knowledge_base_id = file.meta.get('collection_name') if file.meta else None if knowledge_base_id: knowledge_bases = await Knowledges.get_knowledge_bases_by_user_id(user.id, access_type, db=db) for knowledge_base in knowledge_bases: - if knowledge_base.id == knowledge_base_id: + if knowledge_base.id == knowledge_base_id and ( + access_type == 'read' or knowledge_base.user_id == file.user_id + ): return True # Check if the file is associated with any channels the user has access to @@ -78,12 +86,14 @@ async def has_access_to_file( if accessible_ids: return True - # Check if the file is directly attached to a shared workspace model + # Check if the file is directly attached to a shared workspace model (per the ownership + # note above, model write is conferred only for files the model owner owns). for model in await Models.get_models_by_user_id(user.id, permission=access_type, db=db): knowledge_items = getattr(model.meta, 'knowledge', None) or [] for item in knowledge_items: if isinstance(item, dict) and item.get('type') == 'file' and item.get('id') == file.id: - return True + if access_type == 'read' or model.user_id == file.user_id: + return True return False