fix(kanban): refuse to clean up paths outside scratch root (#28818)#29242
fix(kanban): refuse to clean up paths outside scratch root (#28818)#29242Jiahui-Gu wants to merge 1 commit into
Conversation
|
Thanks @alt-glitch for the cross-reference to #28819. Both PRs add containment guards to
Happy to close in favor of #28819 if its shape is preferred, or to align the implementations if you'd like the more defensive variant here. Either way, the security improvement should land — flagging so maintainers can pick whichever they prefer. |
|
Closing as superseded by #31708 (merged: commits 80ad160 + 23115b5 + ce529d6 on main). Your containment fix targets the same site with the same intent. We picked @briandevans's #28819 because it covers more managed-scratch roots (env override + per-board workspaces) and rejects sibling metadata subtrees explicitly. Your symlink-unlink branch is a sensible defense — we'll fold that into a follow-up if the broader symlink-protection cluster needs it. Thanks — concurrent independent discovery on a real P0. |
Summary
Fixes #28818 — data-loss bug.
hermes_cli/kanban_db.py::_cleanup_workspaceranshutil.rmtree(workspace_path)for any task whoseworkspace_kind == \"scratch\"(the DB default), butworkspace_pathis whatever the board/task config says — including a user-supplied real source directory (boards set-default-workdir) or a symlink inside the scratch root that points at~/src. Either turned task completion intorm -rfon real data.Fix
_cleanup_workspace(minimal change):workspaces_root()withresolve(strict=False)(collapses symlinks).resolved.is_relative_to(scratch_root)andresolved != scratch_root; otherwise log a warning and return without deleting.wp.is_symlink()so a symlinked scratch entry can never escape viashutil.rmtreesymlink-follow.relative_to(...)+ValueError.The orthogonal hardening (reject scratch-task create-time when workdir is outside
workspaces_root) is left for a follow-up — out of scope for a minimal data-loss fix.Test plan
TestScratchCleanupSafetyintests/hermes_cli/test_kanban_db.py— 4 cases:real_source/dir outside scratch root is preservedreal_source/does not get followed🤖 Generated with Claude Code