fix(personal): scope RAG file delete to the caller's own upload dir (#4602)

The DELETE /api/personal/file disk-delete containment check used the
shared PERSONAL_UPLOADS_DIR root, so one admin could delete another
user's personal upload by passing its path (uploads are partitioned per
owner under <root>/<owner>/). Confine the check to the caller's own
per-owner subdir via _personal_upload_dir_for_owner(owner). RAG removal
and listing exclusion are unchanged (they still serve non-upload indexed
sources). Adds a regression test for the cross-owner case.
This commit is contained in:
nopoz
2026-06-19 15:50:15 -07:00
committed by GitHub
parent ed18192a8e
commit 160267417e
2 changed files with 25 additions and 2 deletions
+4 -2
View File
@@ -358,11 +358,13 @@ def setup_personal_routes(personal_docs_manager, rag_manager, rag_available):
except Exception as e:
logger.warning(f"RAG removal failed for {filepath}: {e}")
# Delete file from disk if it's in uploads dir
# Delete file from disk if it's in the caller's own uploads dir.
# Scope to the per-owner subdir, not the shared uploads root, so one
# admin can't delete another user's personal files by path.
deleted_from_disk = False
try:
abs_target = os.path.realpath(filepath)
base_abs = os.path.realpath(UPLOADS_DIR)
base_abs = os.path.realpath(_personal_upload_dir_for_owner(owner, create=False))
in_uploads = (
abs_target == base_abs
or os.path.commonpath([abs_target, base_abs]) == base_abs
@@ -72,3 +72,24 @@ def test_delete_file_removes_regular_file_inside_upload_root(tmp_path, monkeypat
assert not uploaded_file.exists()
assert docs.excluded == [filepath]
assert rag.deleted_sources == [filepath]
def test_delete_file_refuses_other_owners_upload(tmp_path, monkeypatch):
# alice must not be able to delete a file living under bob's per-owner
# upload subdir, even though it sits inside the shared uploads root.
uploads = tmp_path / "uploads"
uploads.mkdir()
victim = uploads / "bob" / "secret.txt"
victim.parent.mkdir()
victim.write_text("keep me", encoding="utf-8")
docs = _FakePersonalDocs()
rag = _FakeRAG()
monkeypatch.setattr(personal_routes, "UPLOADS_DIR", str(uploads))
monkeypatch.setattr(personal_routes, "get_rag_manager", lambda: rag)
filepath = str(victim)
result = asyncio.run(_delete_endpoint(docs)(filepath=filepath, owner="alice", _admin=None))
assert result["deleted_from_disk"] is False
assert victim.read_text(encoding="utf-8") == "keep me"