fix(security): harden curator rollback tar extraction (symlink/abs-path escape) - #43942
fix(security): harden curator rollback tar extraction (symlink/abs-path escape)#43942zapabob wants to merge 1 commit into
Conversation
The curator rollback extract path validated only member.name against a POSIX-absolute / ".." check before falling back to a plain tf.extractall() on Python < 3.12 (no filter="data"). That left two escape vectors when extracting a crafted snapshot tarball: - symlink/hardlink members: the traversal lives in member.linkname, which was never inspected. The fallback would create the link and a later member could be written *through* it, outside skills/. - Windows absolute / drive-letter / UNC paths (C:/x, \\server\share): the POSIX-only "starts with /" check let these through, and Path(name).parts shows no "..". Add _assert_safe_member(), which rejects unsafe member names (POSIX abs, Windows abs/drive/UNC, ..) AND unsafe link targets, mirroring the secure extraction pattern already used in hermes_cli/profiles.py. The filter="data" path on 3.12+ is unchanged. Relative, in-tree symlinks still extract, so legitimate snapshots are unaffected. Adds regression tests for the path-guard variants, symlink-escape rejection, and in-tree symlink allowance. Co-authored-by: Cursor <cursoragent@cursor.com>
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Security: Hardened curator rollback tar extraction (symlink/abs-path escape)
- Clean fix — adds
_is_unsafe_archive_path()covering POSIX absolute, Windows absolute/drive/UNC paths, and..traversal. Previously onlyname.startswith("/")was checked, missing Windows paths on POSIX hosts and link targets entirely. - Defense-in-depth —
_assert_safe_member()now checksmember.linknamefor symlink/hardlink escapes, addressing the pre-3.12filter="data"fallback gap where a malicious tarball could symlink to/etc/cron.dand write through it on a later member. - Tests cover the full threat surface — 8 unsafe variants including all OS path types, symlink escape, and a safe-relative-symlink sanity check.
- No regression risk: the existing Python 3.12
filter="data"path is unchanged; only the fallback branch gets the stronger guard.
Reviewed by Hermes Agent
|
Verification Review — looks solid ✅ Reviewed the curator rollback tar extraction hardening. Key observations:
No findings. |
|
Thanks for the focused hardening. The premise remains valid on current main: The proposed GitHub currently reports this PR as conflicting, but the current-main changes to Automated hermes-sweeper review. |
|
suggesting changes
The absolute, drive/UNC, and escaping-target checks work for the covered cases, Security evidence:
Not checked:
Signed: GPT-5.6-sol-xhigh in Codex |
Summary
agent/curator_backup.py::rollback()extracts a snapshot tarball back into~/.hermes/skills/. Before extraction it validated onlymember.namewith aPOSIX-absolute /
..check, then fell back to a plaintf.extractall()onPython < 3.12 (where
filter="data"is unavailable). That leaves two escapevectors if an attacker can plant a crafted
skills.tar.gzin.curator_backups/:member.linkname,which was never inspected. The pre-3.12 fallback creates the link, and a
later member can then be written through it, outside
skills/.C:/x,C:\x,\\server\share) — the POSIX-onlystartswith("/")check lets thesethrough, and
Path(name).partsshows no..for them.Fix
_assert_safe_member()(with helper_is_unsafe_archive_path()) thatrejects unsafe member names (POSIX abs, Windows abs/drive/UNC,
..)and unsafe link targets, mirroring the secure extraction pattern
already used in
hermes_cli/profiles.py::_safe_extract_profile_archive.filter="data"path on Python 3.12+ is unchanged.unaffected (no functional degradation).
Test plan
tests/agent/test_curator_backup.py— new tests:test_is_unsafe_archive_path_variants(POSIX abs, Windows abs/drive/UNC,..)test_rollback_rejects_symlink_escape(symlink with absolute target is refused)test_assert_safe_member_allows_in_tree_symlink(relative in-tree link allowed)test_rollback_rejects_unsafe_tarballstill passes.