T05 · Unauthorized Access and Privilege Escalation
Error
- Location
- scripts/extract_skill.py:50
- Finding
- Workspace Boundary Bypass Through Symlinked Output Directories## Vulnerability Details **File Location**: `scripts/extract_skill.py:50-58` and `scripts/extract_skill.py:180-205` **Vulnerability Type**: Insufficient path-containment validation and symlink traversal **Risk Level**: High ### Vulnerable 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 cleaned ``` ```python 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/skills` is a symlink to an external directory, the computed `root / "skills" / args.name` path is textually beneath the workspace but resolves outside it. This is espe ...[truncated 1887 chars]
- Remediation
- ## Remediation Suggestions 1. 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") ``` 2. Inspect every path component with `lstat()` and reject symbolic links in the output path. 3. Repeat the containment check immediately before deletion and immediately before file creation to reduce time-of-check/time-of-use exposure. 4. Refuse destructive overwrite when the destination is a symlink or contains symlinked ancestors. 5. Replace the custom recursive deletion loop with a hardened deletion routine that explicitly enforces the approved root. 6. Require explicit confirmation or a narrowly scoped overwrite option before deleting an existing directory. 7. Add tests covering: - A symlinked `--output-dir`. - A symlinked Skill directory. - Nested symlink components. - `--force` against destinations outside the root. - Symlink replacement between validation and write operations.
