T05 · Unauthorized Access and Privilege Escalation
Error
- Location
- scripts/scanner.py:321
- Finding
- Symlink Following Enables Arbitrary Local File Exfiltration<![CDATA[ ## Vulnerability Details **File Location**: `scripts/scanner.py:321-330`, with transmission at `scripts/scanner.py:421-456` **Vulnerability Type**: Symlink-assisted access outside the scan root **Risk Level**: Critical ### Vulnerable Code ```python def pack_zip(skill_dir: Path) -> bytes: """Pack a skill directory into a zip byte stream, excluding redundant directories.""" import io buf = io.BytesIO() with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: for p in sorted(skill_dir.rglob("*")): if any(part in SKIP_DIRS for part in p.relative_to(skill_dir).parts): continue if p.is_file() and p.name not in SKIP_FILES: zf.write(p, p.relative_to(skill_dir)) return buf.getvalue() ``` The resulting archive is transmitted here: ```python def cloud_upload(skill_dir, name, dir_hash): """Step 2: Upload skill (multipart/form-data), returns task_no.""" # Pack the entire directory for full code context zip_data = pack_zip(skill_dir) filename = "%s.zip" % name ``` ```python req = urllib.request.Request(API_UPLOAD, data=body, headers=headers, method="POST") try: with urllib.request.urlopen(req, timeout=60) as r: resp = json.loads(r.read().decode("utf-8", errors="replace")) ``` ### Technical Analysis `Path.is_file()` follows symbolic links. `ZipFile.write()` subsequently opens and reads the link target rather than archiving only the symbolic-link metadata. The scanner does not: - Reject symbolic links. - Resolve each candidate and verify that it remains under `skill_dir`. - Restrict the packaged content to an explicit safe-file allowlist. - Inspect the final archive manifest before transmission. Consequently, an attacker-controlled Skill can contain a symbolic link whose apparent location is inside the Skill while its target is any file readable by the scanner process. The skip rules only examine the apparent relative path. A lin ...[truncated 1864 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Reject symbolic links before reading or archiving files: ```python if p.is_symlink(): raise ValueError(f"Symbolic links are not permitted: {p}") ``` 2. Resolve every candidate and enforce containment with `Path.is_relative_to()`: ```python root = skill_dir.resolve() resolved = p.resolve(strict=True) if not resolved.is_relative_to(root): raise ValueError(f"Path escapes scan root: {p}") ``` 3. Apply the same containment checks to hashing, text collection, copying, and archive creation. 4. Use an explicit allowlist of reviewable source-code file types rather than archiving every file. 5. Display the complete archive manifest and total size before any upload. 6. Add per-file and total-size limits to reduce accidental disclosure and denial-of-service risks. 7. Open files with platform-appropriate no-follow protections where available to mitigate link-swap race conditions. 8. Add regression tests covering symlinks to files, symlinks to directories, nested links, broken links, and links changed during scanning. ]]>
