T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/run_eval.py:34
- Finding
- Path Traversal Enables Artifact Creation Outside the Intended Run Directory## Vulnerability Details **File Location**: `scripts/run_eval.py:34-38` **Vulnerability Type**: Unsanitized path construction and path traversal **Risk Level**: Medium ### Vulnerable Code ```python def resolve_run_dirs(skill_dir: Path, run_group: str | None, mode: str) -> tuple[str, Path]: resolved_group = run_group or run_stamp() run_root = skill_dir / "evals" / "runs" / resolved_group run_dir = run_root / mode run_dir.mkdir(parents=True, exist_ok=False) return resolved_group, run_dir ``` The resulting uncontained directory is subsequently used for writes at `scripts/run_eval.py:342-344`: ```python (run_dir / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n") (run_dir / "summary.md").write_text(make_summary_md(skill_dir.name, summary)) (run_dir / "run_metadata.json").write_text(json.dumps(run_metadata, ensure_ascii=False, indent=2) + "\n") ``` ### Technical Analysis The user-controlled `--run-group` argument is appended directly to the nominal `evals/runs` root. The implementation does not reject absolute paths, parent-directory components such as `..`, or path separators. It also does not resolve the resulting path and verify that it remains beneath the authorized run root. In Python's `pathlib`, parent-directory components can escape the intended root, while joining an absolute path can discard the preceding path components entirely. Consequently, `run_dir.mkdir()` and the subsequent `write_text()` operations can act outside the target Skill directory. The `mode` component is constrained by `argparse` choices, but this does not mitigate traversal through `resolved_group`. ### Attack Path 1. The attacker can invoke `run_eval.py` and control `--run-group`. 2. The attacker supplies an absolute path or a traversal sequence, for example: ```bash python3 scripts/run_eval.py /path/to/skill \ --run-group ../../../../attacker-selected-dire ...[truncated 1312 chars]
- Remediation
- ## Remediation Suggestions 1. Restrict run-group names to simple identifiers: ```python import re RUN_GROUP_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$") def validate_run_group(value: str) -> str: if value in {".", ".."} or not RUN_GROUP_PATTERN.fullmatch(value): raise SystemExit( "Error: run-group may contain only letters, digits, dots, " "underscores, and hyphens." ) return value ``` 2. Resolve and enforce containment before creating anything: ```python runs_root = (skill_dir / "evals" / "runs").resolve() resolved_group = validate_run_group(run_group or run_stamp()) run_root = (runs_root / resolved_group).resolve() if not run_root.is_relative_to(runs_root): raise SystemExit("Error: run-group escapes the evals/runs directory.") ``` 3. Perform the same containment check on `run_dir` before calling `mkdir()` or writing files. 4. If target directories may be writable by untrusted users, reject symlinked path components or open files through directory-relative, no-follow mechanisms to reduce symlink race risks. 5. Add regression tests covering absolute paths, `../` traversal, nested separators, `.`, `..`, and symlink-based escape attempts.
