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) <noreply@anthropic.com>
This commit is contained in:
1 file changed
+22
-12
@@ -38,25 +38,33 @@ async def has_access_to_file(
|
|||||||
if file.user_id == user.id:
|
if file.user_id == user.id:
|
||||||
return True
|
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)
|
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)}
|
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:
|
for knowledge_base in knowledge_bases:
|
||||||
if knowledge_base.user_id == user.id or await AccessGrants.has_access(
|
if (
|
||||||
user_id=user.id,
|
knowledge_base.user_id == user.id
|
||||||
resource_type='knowledge',
|
or await AccessGrants.has_access(
|
||||||
resource_id=knowledge_base.id,
|
user_id=user.id,
|
||||||
permission=access_type,
|
resource_type='knowledge',
|
||||||
user_group_ids=user_group_ids,
|
resource_id=knowledge_base.id,
|
||||||
db=db,
|
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
|
return True
|
||||||
|
|
||||||
knowledge_base_id = file.meta.get('collection_name') if file.meta else None
|
knowledge_base_id = file.meta.get('collection_name') if file.meta else None
|
||||||
if knowledge_base_id:
|
if knowledge_base_id:
|
||||||
knowledge_bases = await Knowledges.get_knowledge_bases_by_user_id(user.id, access_type, db=db)
|
knowledge_bases = await Knowledges.get_knowledge_bases_by_user_id(user.id, access_type, db=db)
|
||||||
for knowledge_base in knowledge_bases:
|
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
|
return True
|
||||||
|
|
||||||
# Check if the file is associated with any channels the user has access to
|
# 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:
|
if accessible_ids:
|
||||||
return True
|
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):
|
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 []
|
knowledge_items = getattr(model.meta, 'knowledge', None) or []
|
||||||
for item in knowledge_items:
|
for item in knowledge_items:
|
||||||
if isinstance(item, dict) and item.get('type') == 'file' and item.get('id') == file.id:
|
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
|
return False
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user