T05 · Unauthorized Access and Privilege Escalation
- Location
scripts/extract_skill.py:50- Finding
Workspace Boundary Bypass Through Symlinked Output Directories
- Content
View full analysis
Vulnerability Details
File Location:
scripts/extract_skill.py:50-58andscripts/extract_skill.py:180-205
Vulnerability Type: Insufficient path-containment validation and symlink traversal
Risk Level: HighVulnerable Code
python def validate_output_dir(output_dir: str) -> str: if output_dir.startswith("/"): raise CliError("--output-dir must be relative to --root") if ".." in Path(output_dir).parts: raise CliError("--output-dir must not contain '..' path segments") cleaned = output_dir.strip() or "skills" return cleanedpython root = resolve_root(args.root) output_dir = validate_output_dir(args.output_dir) skill_path = root / output_dir / args.name if skill_path.exists() and not args.force: raise CliError(f"Skill path already exists: {skill_path}. Use --force to overwrite.") preview = { "ok": True, "workspace_root": str(root), "skill_path": str(skill_path), "scaffold_evals": args.scaffold_evals, } if args.dry_run: preview["sketch"] = skill_template(args) print_output(preview, args.format) return 0 if skill_path.exists() and args.force: for child in sorted(skill_path.rglob("*"), reverse=True): if child.is_file(): child.unlink() elif child.is_dir(): child.rmdir() created = create_files(skill_path, args)Technical Analysis
The output-directory validation only rejects absolute path strings and explicit
..components. It does not resolve the final destination and verify that it remains beneath the resolved workspace root.Consequently, a directory component beneath the workspace can be a symbolic link to a location outside the workspace. For example, if
workspace/skillsis a symlink to an external directory, the computedroot / "skills" / args.namepath is textually beneath the workspace but resolves outside it.This is espe ...[truncated 1887 chars]
- Remediation
View remediation
Remediation Suggestions
- Resolve the workspace root and final destination before performing any operation:
python root = resolve_root(args.root) skill_path = (root / output_dir / args.name).resolve() try: skill_path.relative_to(root) except ValueError: raise CliError("Resolved skill path escapes the workspace root") - Inspect every path component with
lstat()and reject symbolic links in the output path. - Repeat the containment check immediately before deletion and immediately before file creation to reduce time-of-check/time-of-use exposure.
- Refuse destructive overwrite when the destination is a symlink or contains symlinked ancestors.
- Replace the custom recursive deletion loop with a hardened deletion routine that explicitly enforces the approved root.
- Require explicit confirmation or a narrowly scoped overwrite option before deleting an existing directory.
- Add tests covering:
- A symlinked
--output-dir. - A symlinked Skill directory.
- Nested symlink components.
--forceagainst destinations outside the root.- Symlink replacement between validation and write operations.
- A symlinked
- Resolve the workspace root and final destination before performing any operation:
