T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/pack_skill.py:156
- Finding
- Out-of-Root File Disclosure Through Symlink Following During Packaging## Vulnerability Details **File Location**: `scripts/pack_skill.py:156-167`, `scripts/pack_skill.py:197-198`, and `scripts/pack_skill.py:343-347` **Vulnerability Type**: Symlink-based arbitrary local file disclosure **Risk Level**: High ### Technical Analysis The packager collects file paths from the selected skill directory without rejecting symbolic links or verifying that each resolved path remains beneath the resolved skill root: ```python p = d / fn ... keep.append(p) ``` The collected paths are subsequently passed to `ZipFile.write()`: ```python for f in files: zf.write(f, f.relative_to(root.parent)) ``` They are also copied into the local skill installation: ```python for f in files: dst = target / f.relative_to(root) dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(f, dst) ``` The archive name is calculated from the lexical path using `relative_to()`, but the source file is opened through `f`. For a file symlink, the file operations follow the symlink and read its target. Consequently, a path that appears to be inside the skill directory can refer to a file anywhere readable by the victim account. The existing path-containment checks identified elsewhere in the script protect the relationship between installation and source directories, but they do not validate the resolved target of every file collected for packaging. ### Attack Path 1. An attacker supplies or modifies a skill directory that the victim will audit and package. 2. The attacker places a file symlink in that directory, such as a normal-looking configuration or reference file, pointing to a sensitive file outside the skill root. 3. The victim invokes the documented packaging workflow on the skill directory. 4. `collect_packable()` accepts the symlink as a file and adds its lexical path to the package list. 5. `ZipFile.write()` follows the symlink and embeds the target file's contents under the at ...[truncated 1005 chars]
- Remediation
- ## Remediation Suggestions 1. Reject all symbolic links while collecting package contents: ```python if p.is_symlink(): raise ValueError(f"Symbolic links are not permitted: {p}") ``` 2. Resolve every candidate with `resolve(strict=True)` and require it to remain under `root.resolve(strict=True)` before reading it. 3. Repeat containment and file-type validation immediately before archiving or copying to reduce time-of-check/time-of-use replacement opportunities. 4. Where platform support permits, open source files with no-follow semantics and archive data from the securely opened file descriptor rather than reopening a pathname. 5. Apply equivalent checks to directories and every path component, not only final file entries. 6. Add regression tests covering file symlinks, directory symlinks, dangling symlinks, symlink chains, and concurrent replacement attempts.
