T09 · Insecure Skill Coding Practices
- Location
scripts/generate_qwen3_voices.py:28- Finding
Manifest-Controlled Path Traversal Allows Files to Be Written Outside the Project
- Content
View full analysis
Vulnerability Details
File Location:
scripts/generate_qwen3_voices.py, lines 28–32 and 74–75; related incomplete validation inscripts/validate_contract.py, lines 9–15
Vulnerability Type: Arbitrary file write through insufficient path validation
Risk Level: MediumVulnerable Code
python data = json.loads(args.manifest.read_text(encoding="utf-8")) out_dir = Path(data["output_dir"]) if not out_dir.is_absolute(): out_dir = args.manifest.resolve().parent / out_dir out_dir.mkdir(parents=True, exist_ok=True)python target = out_dir / segment["file"] sf.write(target, wavs[0], sample_rate)The related contract validator only validates the directory and does not independently validate the segment filename:
python def normalized_output(output_dir: str, filename: str) -> str: directory = PurePosixPath(output_dir.replace("\\", "/")) if directory.is_absolute() or ".." in directory.parts: raise ValueError("output_dir must be a safe project-relative path") if directory.parts and directory.parts[0] == "public": directory = PurePosixPath(*directory.parts[1:]) return str(directory / filename)Technical Analysis
The voice manifest is treated as trusted when output paths are constructed. An absolute
output_diris explicitly accepted. A relative directory containing traversal components is also accepted by the generator because the resulting path is not resolved and checked against the project root.The
segment["file"]value is likewise not constrained to a simple filename. It may contain../components or be an absolute path. In Python, joining aPathwith an absolute second operand discards the original base path. Consequently, the finaltargetcan point anywhere writable by the process.Although
validate_contract.pyrejects an absolute or traversingoutput_dir, it does not validatefilename. More importantly, the d ...[truncated 1370 chars]- Remediation
View remediation
Remediation Suggestions
- Define the manifest directory or project root as the only permitted output boundary.
- Reject absolute
output_dirandfilevalues. - Reject empty filenames,
.and..components, path separators in segment filenames, and filenames not ending in.wav. - Resolve the final path and verify containment before creating directories or writing data:
python project_root = args.manifest.resolve().parent output_dir = (project_root / str(data["output_dir"])).resolve() if not output_dir.is_relative_to(project_root): raise SystemExit("output_dir must remain inside the project") filename = Path(str(segment["file"])) if ( filename.is_absolute() or filename.name != str(filename) or filename.suffix.lower() != ".wav" ): raise SystemExit("segment file must be a simple .wav filename") target = (output_dir / filename).resolve() if not target.is_relative_to(output_dir): raise SystemExit("segment output escapes output_dir") - Apply the same validation inside both the generator and contract validator. Validation in a separate command must not be the only security boundary.
- Refuse to replace existing files by default. Require an explicit
--overwriteoption where replacement is intended. - Consider writing to a temporary file within the validated output directory and atomically renaming it after successful generation.
