T09 · Insecure Skill Coding Practices
- Location
scripts/convert.py:149- Finding
Symlink-Aware Output Resolution Can Cause Unintended Recursive Directory Deletion
- Content
View full analysis
Vulnerability Details
File Location:
scripts/convert.py:149-159andscripts/convert.py:385-388
Vulnerability Type: Unsafe recursive directory deletion
Risk Level: MediumVulnerable Code
python def ensure_output_dir(output_dir: Path, overwrite: bool) -> None: if not output_dir.exists(): return if not output_dir.is_dir(): raise ValueError(f"Output path exists and is not a directory: {output_dir}") if not overwrite: raise ValueError( f"Output directory already exists: {output_dir}\\n" "Choose a different output directory or pass --overwrite to replace it." ) shutil.rmtree(output_dir)The user-controlled output path is resolved before it reaches the deletion operation:
python output_dir = ( Path(args.output_dir).expanduser().resolve() if args.output_dir else epub_file.with_suffix("") )Technical Analysis
The optional output path is accepted from the command line and canonicalized with
Path.resolve(). Resolution follows symbolic links and produces the path of the link target. When--overwriteis enabled,ensure_output_dir()performsshutil.rmtree()directly on that resolved path.The implementation verifies only that the resolved path is a directory. It does not:
- Reject an output path that is a symbolic link.
- Verify that the directory was previously generated by this converter.
- Restrict deletion to a designated output root.
- Protect sensitive paths such as a home directory, project directory, or filesystem root.
- Request confirmation after displaying the canonical deletion target.
Consequently, a path that appears to identify an EPUB export directory may resolve to a different writable directory, which is then recursively removed.
Attack Path
- An attacker creates a symbolic link at a plausible output location, such as
./converted-book, poi ...[truncated 1310 chars]
- Remediation
View remediation
Remediation Suggestions
- Check the original lexical output path for symbolic links before calling
resolve()and reject any symlink component. - Add a converter-specific marker file when creating output directories, then require and validate that marker before allowing recursive replacement.
- Refuse dangerous targets, including the filesystem root, user home directory, current project root, and the EPUB's containing directory.
- Display the canonical deletion target and require explicit confirmation in interactive use.
- Prefer deleting only known generated entries such as
META.md,chapters/, andimages/rather than recursively removing an arbitrary directory. - Prepare conversion output in a new temporary directory and replace the old generated export only after conversion succeeds.
- Where feasible, require the output directory to remain beneath an explicitly approved export root.
Example defensive logic should validate the unresolved path and marker before deletion:
python def ensure_output_dir(output_dir: Path, overwrite: bool) -> None: if output_dir.is_symlink(): raise ValueError("Refusing to use a symbolic link as the output directory") if not output_dir.exists(): return if not output_dir.is_dir(): raise ValueError("Output path is not a directory") if not overwrite: raise ValueError("Output directory already exists") marker = output_dir / ".epub-to-markdown-output" if not marker.is_file(): raise ValueError("Refusing to delete an unverified output directory") shutil.rmtree(output_dir)- Check the original lexical output path for symbolic links before calling
