T09 · Insecure Skill Coding Practices
- Location
scripts/apply.py:53- Finding
Workspace migration follows symlinked directories and can move files outside the workspace
- Content
View full analysis
Vulnerability Details
File Location:
scripts/apply.py, lines 53–78 and 177–180
Vulnerability Type: Unrestricted filesystem migration through symlink traversal
Risk Level: HighVulnerable Code
python # 2. Flatten old _tmp/_notes/... directories into files/{new} for old_dir_name, new_sub in OLD_TO_NEW.items(): old_dir = workspace / old_dir_name if not old_dir.is_dir(): continue for child in sorted(old_dir.iterdir()): target = files_dir / new_sub / child.name if not target.exists(): actions.append({ "type": "move", "source": child, "target": target, "dest": f"{FILES_DIR}/{new_sub}", "reason": f"merge from {old_dir_name}", }) # Remove old dir after emptying it actions.append({ "type": "remove_dir", "path": old_dir, "name": old_dir_name, "reason": "replaced by files/", })The planned move is subsequently executed without validating either endpoint:
python elif a["type"] == "move": rel = a["source"].name if dry_run: print(f" [MOVE] {rel:35s} → {a['dest']}/ ({a['reason']})") else: a["target"].parent.mkdir(parents=True, exist_ok=True) a["source"].rename(a["target"]) print(f" ✅ Moved {rel:35s} → {a['dest']}/")Technical Analysis
Path.is_dir()andPath.iterdir()follow directory symlinks. The script does not reject symlinks or verify that resolved source and destination paths remain beneath the selected workspace.Consequently, a legacy directory such as
_tmp,_notes, or_archivecan be a symlink to a directory outside the workspace. The planner will enumerate the external directory as though it were local workspace content. During execution,Path.rename()then moves the external directory's children to the calculated destination.The destination tree is similarly unprotected. If
filesor one of its category director ...[truncated 1729 chars]- Remediation
View remediation
Remediation Suggestions
- Reject symlinks for the workspace,
files, legacy directories, migration sources, destination parents, and destination paths. - Resolve each source and destination and enforce workspace containment before planning and immediately before mutation:
python def require_within_workspace(path: Path, workspace: Path) -> Path: root = workspace.resolve(strict=True) resolved = path.resolve(strict=True) if resolved != root and root not in resolved.parents: raise ValueError(f"Path escapes workspace: {path}") return resolved- Use
Path.lstat()orPath.is_symlink()beforeis_dir()anditerdir()so that directory symlinks are detected rather than followed. - Validate every component of destination paths because checking only the final lexical path does not detect symlinked parents.
- Repeat validation directly before
rename()to reduce time-of-check/time-of-use race exposure. - Where supported, use descriptor-relative filesystem operations with no-follow semantics instead of path-based checks alone.
- Abort execution on any boundary violation and report the unsafe item rather than silently skipping it.
- Add automated tests covering symlinked legacy directories, symlinked
filesdirectories, nested symlink parents, broken symlinks, and paths outside the workspace.
- Reject symlinks for the workspace,
