fix: exclude escaping symlinks from skill manifests (#12821)

This commit is contained in:
Harsh Kashyap
2026-09-22 14:43:47 +02:00
committed by GitHub
parent a34b60e51d
commit fc65634f0a
3 changed files with 19 additions and 2 deletions
@@ -201,11 +201,11 @@ class FileSystemSkillStore:
:returns: Sorted list of POSIX-style paths relative to the skill directory. Empty when there are none.
:raises KeyError: If no skill with `name` exists.
"""
skill_dir = self._skill_dir(name)
skill_dir = self._skill_dir(name).resolve()
return sorted(
p.relative_to(skill_dir).as_posix()
for p in skill_dir.rglob("*")
if p.is_file() and p.name != SKILL_FILE_NAME
if p.is_file() and p.name != SKILL_FILE_NAME and p.resolve().is_relative_to(skill_dir)
)
def read_skill_file(self, name: str, path: str) -> str | ImageContent | FileContent:
@@ -0,0 +1,5 @@
---
fixes:
- |
Fixed ``FileSystemSkillStore.load_skill`` including symlinks that resolve outside the skill directory in its
bundled-file manifest. Escaping symlinks are now excluded, matching ``read_skill_file`` traversal protection.
@@ -205,6 +205,18 @@ class TestFileSystemSkillStore:
store = FileSystemSkillStore(tmp_path)
assert store.load_skill("pdf-forms") == ("Step 1. Do the thing.", [])
@pytest.mark.skipif(sys.platform == "win32", reason="symlinks require elevated privileges on Windows")
def test_load_skill_excludes_symlinks_outside_skill_directory(self, tmp_path):
skill_dir = _write_skill(tmp_path, "pdf-forms", description="d", files={"inside.md": "inside"})
outside = tmp_path / "outside.txt"
outside.write_text("secret", encoding="utf-8")
(skill_dir / "outside-link.txt").symlink_to(outside)
store = FileSystemSkillStore(tmp_path)
_, files = store.load_skill("pdf-forms")
assert files == ["inside.md"]
def test_load_skill_unknown_raises(self, tmp_path):
_write_skill(tmp_path, "pdf-forms", description="d")
store = FileSystemSkillStore(tmp_path)