T09 · Insecure Skill Coding Practices
- Location
scripts/swmm_runner.py:279- Finding
Unvalidated Output Names Permit Path Traversal and Arbitrary File Overwrite
- Content
View full analysis
Vulnerability Details
File Location:
scripts/swmm_runner.py, lines 275–289 and 380–381
Vulnerability Type: Path traversal and unconstrained filesystem write
Risk Level: MediumVulnerable Code
python def cmd_run(args): inp = args.inp.resolve() run_dir = args.run_dir.resolve() run_dir.mkdir(parents=True, exist_ok=True) rpt = run_dir / (args.rpt_name or "model.rpt") out = run_dir / (args.out_name or "model.out") stdout_path = run_dir / "stdout.txt" stderr_path = run_dir / "stderr.txt" timeout = getattr(args, "timeout", DEFAULT_SWMM_TIMEOUT_S) rc = run_swmm(inp, rpt, out, stdout_path, stderr_path, timeout=timeout)The output names are accepted directly from command-line arguments:
python ap_run.add_argument('--rpt-name', default=None) ap_run.add_argument('--out-name', default=None)The resulting paths are supplied to the SWMM process:
python p = subprocess.run( [resolve_swmm5(), str(inp), str(rpt), str(out)], capture_output=True, text=True, timeout=timeout, )Technical Analysis
The values of
--rpt-nameand--out-nameare not restricted to plain filenames. Python'spathlibpermits both traversal components and absolute paths:- A value such as
../../target.rptescapes the selected run directory. - An absolute value such as
/home/user/target.rptsupersedesrun_direntirely.
No canonicalization or containment check verifies that the final report and output paths remain below
run_dir. The paths are then passed toswmm5, which may create or overwrite files at those locations using the privileges of the user running the skill.Although
subprocess.runuses an argument list and therefore does not introduce shell-command injection, it does not prevent filesystem path traversal. The vulnerability violates the documented expectation that generated artifacts remain in the standard run directory.Attack Path
- An attacker controls or influences ...[truncated 1388 chars]
- A value such as
- Remediation
View remediation
Remediation Suggestions
Require output-name arguments to be plain basenames and verify canonical containment before invoking
swmm5.python def safe_output_path(run_dir: Path, name: str) -> Path: candidate_name = Path(name) if candidate_name.is_absolute() or candidate_name.name != name: raise ValueError("Output name must be a plain filename") base = run_dir.resolve() candidate = (base / candidate_name).resolve() if candidate.parent != base: raise ValueError("Output path must remain inside the run directory") return candidateApply the validation to both destinations:
python rpt = safe_output_path(run_dir, args.rpt_name or "model.rpt") out = safe_output_path(run_dir, args.out_name or "model.out")Additional hardening should include:
- Reject names containing directory separators,
.or..path components, and platform-specific alternate separators. - Restrict expected extensions to
.rptand.outwhere compatibility permits. - Check for symlinks at destination paths before execution so an existing link cannot redirect writes outside
run_dir. - Use a dedicated, newly created run directory with restrictive permissions.
- Reject collisions with existing files unless overwrite behavior is explicitly requested.
- Apply the same validation at the MCP or API boundary so unsafe paths are rejected before reaching the script.
- Add regression tests covering
../, absolute Unix paths, Windows drive paths, alternate separators, nested names, and symlink destinations.
- Reject names containing directory separators,
