Block out-of-skill symlink hashing in skills scan (#3410)

🤖 Generated with GPT-5.2-Codex
This commit is contained in:
Jeremiah Lowin 2026-03-07 11:40:46 -05:00 committed by GitHub
commit ea19a2a5f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 30 additions and 2 deletions

View file

@ -86,16 +86,22 @@ def compute_file_hash(path: Path) -> str:
def scan_skill_files(skill_dir: Path) -> list[SkillFileInfo]:
"""Scan a skill directory for all files."""
files = []
resolved_skill_dir = skill_dir.resolve()
# Sort for deterministic ordering across platforms
for file_path in sorted(skill_dir.rglob("*")):
if file_path.is_file():
resolved_file_path = file_path.resolve()
if not resolved_file_path.is_relative_to(resolved_skill_dir):
continue
rel_path = file_path.relative_to(skill_dir)
files.append(
SkillFileInfo(
# Use POSIX paths for cross-platform URI consistency
path=rel_path.as_posix(),
size=file_path.stat().st_size,
hash=compute_file_hash(file_path),
size=resolved_file_path.stat().st_size,
hash=compute_file_hash(resolved_file_path),
)
)
return files

View file

@ -169,6 +169,28 @@ This is my skill content.
assert "reference.md" in paths
assert "scripts/helper.py" in paths
async def test_manifest_ignores_symlink_target_outside_skill(self, tmp_path: Path):
skill_dir = tmp_path / "symlinked-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("# Skill\n")
outside_file = tmp_path / "outside.txt"
outside_file.write_text("secret")
(skill_dir / "leak.txt").symlink_to(outside_file)
mcp = FastMCP("Test")
mcp.add_provider(SkillProvider(skill_path=skill_dir))
async with Client(mcp) as client:
result = await client.read_resource(
AnyUrl("skill://symlinked-skill/_manifest")
)
manifest = json.loads(result[0].text)
paths = {f["path"] for f in manifest["files"]}
assert "SKILL.md" in paths
assert "leak.txt" not in paths
async def test_read_supporting_file_via_template(self, single_skill_dir: Path):
mcp = FastMCP("Test")
mcp.add_provider(SkillProvider(skill_path=single_skill_dir))