fix(skills,pairing): path traversal guard in uninstall, lock list_pending, hash file paths
- skills_hub: validate that uninstall_skill's install_path resolves inside SKILLS_DIR before calling shutil.rmtree, preventing recursive deletion of arbitrary directories via poisoned lock.json entries - skills_hub: include file paths (not just contents) in bundle_content_hash so swapping filenames between files changes the hash, strengthening update-detection integrity - pairing: wrap list_pending() in self._lock so _cleanup_expired() file writes don't race with concurrent generate_code()/approve_code() calls Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3000,6 +3000,13 @@ def uninstall_skill(skill_name: str) -> Tuple[bool, str]:
|
||||
return False, f"'{skill_name}' is not a hub-installed skill (may be a builtin)"
|
||||
|
||||
install_path = SKILLS_DIR / entry["install_path"]
|
||||
# Prevent path traversal from poisoned lock.json entries
|
||||
try:
|
||||
resolved = install_path.resolve()
|
||||
if not resolved.is_relative_to(SKILLS_DIR.resolve()):
|
||||
return False, f"Refusing to remove '{entry['install_path']}': resolves outside skills directory"
|
||||
except (ValueError, OSError):
|
||||
return False, f"Refusing to remove '{entry['install_path']}': path resolution failed"
|
||||
if install_path.exists():
|
||||
shutil.rmtree(install_path)
|
||||
|
||||
@@ -3013,6 +3020,10 @@ def bundle_content_hash(bundle: SkillBundle) -> str:
|
||||
"""Compute a deterministic hash for an in-memory skill bundle."""
|
||||
h = hashlib.sha256()
|
||||
for rel_path in sorted(bundle.files):
|
||||
# Include the path so swapping file contents between two paths
|
||||
# changes the hash (avoids filename-swap evading update detection).
|
||||
h.update(rel_path.encode("utf-8"))
|
||||
h.update(b"\x00")
|
||||
content = bundle.files[rel_path]
|
||||
if isinstance(content, bytes):
|
||||
h.update(content)
|
||||
|
||||
Reference in New Issue
Block a user