T09 · Insecure Skill Coding Practices
- Location
scripts/release_guard.py:129- Finding
Unrestricted Release Paths Permit Arbitrary Directory Deletion and Local File Disclosure
- Content
View full analysis
Vulnerability Details
File Location:
scripts/release_guard.py:129-153andscripts/release_guard.py:371-376
Vulnerability Type: Unsafe path handling and insufficient filesystem boundary validation
Risk Level: HighVulnerable Code
python def copy_selected(repo: Path, include_paths: list[str], output_dir: Path, forbidden_patterns: list[str]) -> None: if output_dir.exists(): shutil.rmtree(output_dir) output_dir.mkdir(parents=True, exist_ok=True) for item in include_paths: src = repo / item if not src.exists(): continue if matches_any(item, forbidden_patterns): continue dst = output_dir / item if src.is_dir(): def ignore(dirpath: str, names: list[str]) -> set[str]: ignored: set[str] = set() for name in names: candidate = Path(dirpath) / name rel_path = candidate.relative_to(repo).as_posix() if matches_any(rel_path, forbidden_patterns): ignored.add(name) return ignored shutil.copytree(src, dst, dirs_exist_ok=True, ignore=ignore) else: dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src, dst)The function is invoked using a user-supplied output path:
python if args.prepare_release_dir: if not include_paths: failures.append("publish.include_paths missing in local config, cannot prepare release dir safely") else: output_dir = Path(args.prepare_release_dir).resolve() copy_selected(repo, include_paths, output_dir, forbidden_patterns)Technical Analysis
The
--prepare-release-dirargument is converted into an absolute path and passed directly tocopy_selected(). If that path already exists, the function recursively deletes it with `s ...[truncated 2987 chars]- Remediation
View remediation
Remediation Suggestions
-
Constrain the output directory
- Require the output directory to be beneath an approved temporary root.
- Reject filesystem roots, the user's home directory, the repository itself, and ancestors of the repository.
- Refuse to delete a directory unless it contains a tool-specific marker created during an earlier safe initialization.
- Prefer creating a new directory with
tempfile.mkdtemp()instead of accepting an arbitrary deletion target.
-
Validate every include path
- Reject absolute paths.
- Reject empty entries and paths containing
... - Resolve each source path and verify it remains beneath the resolved repository root using
Path.is_relative_to(). - Resolve each destination parent and verify it remains beneath the output root.
-
Handle symlinks safely
- Reject symlinked source files and directories unless explicitly required.
- Do not follow symlinks during recursive copying.
- Recheck resolved containment immediately before copying to reduce traversal and symlink risks.
-
Fail closed
- Treat any invalid include path as a fatal error instead of silently skipping it.
- Build and validate the complete copy plan before deleting or creating anything.
- Abort publication preparation if any source cannot be proven to reside inside the repository.
-
Add regression tests
- Test absolute include paths, parent traversal, symlinks escaping the repository, filesystem-root output, home-directory output, repository output, and output paths that are ancestors of the repository.
- Verify that all unsafe cases fail before filesystem modifications occur.
-
