T09 · Insecure Skill Coding Practices
- Location
scripts/generate_pregnancy_calendar.py:585- Finding
User-Controlled Output Prefix Allows Path Traversal and File Overwrite
- Content
View full analysis
Vulnerability Details
File Location:
scripts/generate_pregnancy_calendar.py, lines 585 and 596–603, with write operations at lines 641–642
Vulnerability Type: Path traversal and arbitrary file overwrite
Risk Level: MediumVulnerable Code
python parser.add_argument("--prefix", default="pregnancy_reminders", help="Output file prefix.") args = parser.parse_args() # ... out_dir = args.output_dir out_dir.mkdir(parents=True, exist_ok=True) safe_due = due.isoformat() base = f"{args.prefix}_due_{safe_due}" json_path = out_dir / f"{base}.json" ics_path = out_dir / f"{base}.ics" outline_path = out_dir / f"{base}_outline.md" report_path = out_dir / f"{base}_validation.json" # ... json_path.write_text(json.dumps(events, ensure_ascii=False, indent=2), encoding="utf-8") report_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")The other generated paths are also passed to writing functions:
python write_ics(events, ics_path, calendar_name) write_outline( outline_path, events, windows, lmp_input, doctor_due, lmp, due, start, end, calendar_name, args.daily_time, )Technical Analysis
The
--prefixcommand-line value is concatenated into output path names without validation. Python'spathlibdoes not guarantee containment when joining an untrusted path component to a trusted directory. A prefix containing../components can therefore traverse above--output-dir. An absolute prefix can similarly cause the trusted output directory to be discarded by path composition.The program then uses
Path.write_text()and related output functions without checking whether the destination already exists. These operations truncate and replace existing writable files. There is no filename allowlist, resolved-path containment check, or overwrite confirmation.The due-date suffix and fixed output extensions constrain the final filenames, but they do not prevent di ...[truncated 1885 chars]
- Remediation
View remediation
Remediation Suggestions
-
Restrict
--prefixto a safe filename component. For example, accept only letters, digits, periods, underscores, and hyphens:python def parse_prefix(value: str) -> str: if not re.fullmatch(r"[A-Za-z0-9._-]+", value): raise argparse.ArgumentTypeError( "Prefix may contain only letters, digits, periods, underscores, and hyphens" ) if value in {".", ".."}: raise argparse.ArgumentTypeError("Invalid output prefix") return value parser.add_argument( "--prefix", type=parse_prefix, default="pregnancy_reminders", help="Output filename prefix.", ) -
Resolve and verify every destination remains beneath the resolved output directory:
python out_dir = args.output_dir.resolve() out_dir.mkdir(parents=True, exist_ok=True) def contained_output(filename: str) -> Path: candidate = (out_dir / filename).resolve() if candidate.parent != out_dir: raise SystemExit(f"Output path escapes output directory: {candidate}") return candidate -
Use
contained_output()for all four generated paths. -
Refuse to overwrite existing files by default. Add an explicit
--forceoption when replacement is intentional:python for path in (json_path, ics_path, outline_path, report_path): if path.exists() and not args.force: raise SystemExit(f"Output file already exists: {path}") -
For stronger protection against race conditions, create new files using exclusive creation mode (
"x") rather than checking and then writing separately. -
Add regression tests covering:
- Prefixes containing
../and path separators. - Absolute prefixes.
- Existing destination files.
- Symlinks that resolve outside the output directory.
- Valid ordinary prefixes.
- Prefixes containing
-
