T09 · Insecure Skill Coding Practices
- Location
package_skill.py:87- Finding
Symbolic Link Traversal Can Include Files Outside the Skill Directory
- Content
View full analysis
Vulnerability Details
File Location:
package_skill.py:87-94
Vulnerability Type: Archive boundary bypass through symbolic links
Risk Level: MediumVulnerable Code:
python for file_path in path.rglob('*'): if file_path.is_file(): # Skip forbidden files if any(f in str(file_path) for f in ['.git', '__pycache__', '.DS_Store', 'node_modules']): continue arcname = file_path.relative_to(path) zf.write(file_path, arcname)Technical Analysis
The packager recursively enumerates entries under the supplied Skill directory and uses
Path.is_file()to determine whether each entry should be archived. For symbolic links,is_file()follows the link and returns true when its target is a regular file.ZipFile.write()subsequently reads the symbolic link target. Althoughfile_path.relative_to(path)produces an archive name within the Skill directory, it does not prove that the resolved source file is within that directory. Consequently, a symlink inside the Skill can point to an arbitrary readable file elsewhere on the host, and the target's contents will be stored under the symlink's internal archive name.The validation logic does not reject symbolic links and does not compare resolved candidate paths against the resolved Skill root. The forbidden-file substring checks do not provide a reliable boundary control.
Attack Path
- An attacker prepares an otherwise valid Skill directory.
- The attacker adds a symbolic link inside that directory, such as
assets/report.txt, pointing to a sensitive host file. - A victim runs
package_skill.pyagainst the untrusted Skill directory. path.rglob('*')discovers the symlink.file_path.is_file()follows the symlink and accepts it because its target is a regular file.zf.write(file_path, arcname)reads the external target and embeds its contents in the ZIP.- The vi ...[truncated 891 chars]
- Remediation
View remediation
Remediation Suggestions
-
Reject symbolic links explicitly during both validation and packaging:
python if file_path.is_symlink(): raise ValueError(f"Symbolic links are not allowed: {file_path}") -
Resolve the source root and every candidate, then verify that the resolved candidate remains under the resolved root:
python root = path.resolve() for file_path in path.rglob("*"): if file_path.is_symlink(): raise ValueError(f"Symbolic links are not allowed: {file_path}") resolved = file_path.resolve() if not resolved.is_relative_to(root): raise ValueError(f"File escapes skill directory: {file_path}") if resolved.is_file(): arcname = resolved.relative_to(root) zf.write(resolved, arcname) -
Perform the same checks immediately before reading each file to reduce filesystem race risks.
-
Prefer an allowlist of expected files and top-level directories rather than archiving every recursively discovered file.
-
Add automated tests covering symlinks to external files, broken symlinks, nested symlinks, and links to forbidden directories.
-
Inspect the final archive manifest before presenting the package as ready for distribution.
-
