T09 · Insecure Skill Coding Practices
- Location
scripts/package_skill.py:65- Finding
Symbolic Link Allows Packaging Files Outside the Skill Directory
- Content
View full analysis
Vulnerability Details
File Location:
scripts/package_skill.py, lines 65–69
Vulnerability Type: Symlink-based arbitrary local file disclosure
Risk Level: Highpython for file_path in skill_path.rglob('*'): if file_path.is_file(): # Calculate the relative path within the zip arcname = file_path.relative_to(skill_path.parent) zipf.write(file_path, arcname) print(f" Added: {arcname}")Technical Analysis
The packaging routine recursively discovers entries beneath an attacker-supplied skill directory and adds entries for which
Path.is_file()returns true. That check follows symbolic links. The subsequentzipf.write(file_path, arcname)operation also reads the referenced target rather than preserving or rejecting the symbolic link.The archive name is calculated from the unresolved path inside the skill directory. Consequently, a symbolic link appears to be an ordinary in-scope skill file in the archive even when its target is outside the skill root.
The validation performed before packaging does not reject symbolic links or verify that every resolved file remains beneath the resolved skill directory. An untrusted skill author can therefore cause the packager to read any file accessible to the user running it.
Attack Path
- An attacker supplies a skill directory containing a valid
SKILL.md. - The attacker places a symbolic link within that directory, such as
assets/collected-data, pointing to a sensitive local file expected to exist on the packaging system. - The user or agent invokes
scripts/package_skill.pyon the supplied skill directory, as directed by the Skill workflow. rglob('*')discovers the symbolic link, andis_file()accepts it because its target is a regular file.zipf.write()follows the link and copies the target file's contents into the generated ZIP archive under an apparently legitimate skill-relative name.- If the archive is shared or published as ...[truncated 780 chars]
- An attacker supplies a skill directory containing a valid
- Remediation
View remediation
Remediation Suggestions
- Reject symbolic links explicitly before adding entries:
python if file_path.is_symlink(): raise ValueError(f"Symbolic links are not allowed: {file_path}") - Resolve the skill root and every candidate path, then require each candidate to remain beneath the resolved root:
python skill_root = skill_path.resolve() for file_path in skill_root.rglob("*"): if file_path.is_symlink(): raise ValueError(f"Symbolic links are not allowed: {file_path}") resolved = file_path.resolve(strict=True) if not resolved.is_relative_to(skill_root): raise ValueError(f"File escapes skill directory: {file_path}") if resolved.is_file(): arcname = resolved.relative_to(skill_root.parent) zipf.write(resolved, arcname) - Reject special filesystem objects and package regular files only.
- Where supported, open files using operating-system primitives that prevent symlink following, such as
O_NOFOLLOW, to reduce check-to-use race exposure. - Add validation tests covering file symlinks, directory symlinks, links to sensitive external files, broken links, and links changed during packaging.
- Abort packaging on any boundary violation rather than silently skipping questionable entries, so users cannot mistake an unsafe package operation for a successful one.
- Reject symbolic links explicitly before adding entries:
