T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/text_to_speech.py:406
- Finding
- Batch Output Path Traversal Allows Writes Outside the Designated Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/text_to_speech.py`, lines 406-413 **Vulnerability Type**: Path traversal and arbitrary file write **Risk Level**: Medium ### Vulnerable Code ```python explicit_out = job.get("out") if explicit_out: out_path = _normalize_output_path(str(explicit_out), response_format) if out_path.is_absolute(): out_path = out_dir / out_path.name else: out_path = out_dir / out_path else: slug = _slugify(input_text[:80]) out_path = out_dir / f"{idx:03d}-{slug}.{response_format}" ``` ### Technical Analysis The batch job's attacker-controlled `out` property is converted into a path and joined directly to `out_dir`. Although absolute paths are reduced to their basename, relative paths are not checked for parent-directory components such as `..`. For example, the following job produces a path outside the designated output directory: ```json {"input":"Attacker-controlled content","out":"../../target.mp3"} ``` If `--out-dir output/speech` is used, the resulting path is effectively: ```text output/speech/../../target.mp3 ``` The later `_write_audio()` call creates parent directories and streams the API response to this path. Existing files are protected by default, but passing `--force` permits replacement. The code also performs no resolved-path containment check, so path traversal and potentially symlink-based escapes remain possible. ### Attack Path 1. An attacker supplies or modifies a JSONL batch file. 2. The attacker sets a job's `out` property to a traversal path such as `../../target.mp3`. 3. A user invokes `speak-batch` with that JSONL file and a valid API key. 4. The application joins the traversal path to `out_dir` without canonicalization or containment validation. 5. The OpenAI API response is written outside the intended output directory. 6. If the user supplied `--force`, an existing writable file at the resolved destination can be replaced with generated audio da ...[truncated 625 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Reject absolute paths and any path containing `..` components. 2. Resolve both the output directory and candidate destination before writing. 3. Verify that the resolved destination is strictly contained within the resolved output directory. 4. Account for existing symlinks in parent directories and reject destinations that escape through symlink resolution. 5. Continue refusing to overwrite existing files by default. 6. Consider restricting batch `out` values to filenames rather than arbitrary relative paths. Example containment validation: ```python base_dir = Path(args.out_dir).resolve() relative_out = Path(str(explicit_out)) if relative_out.is_absolute() or ".." in relative_out.parts: _die(f"Invalid batch output path: {relative_out}") candidate = (base_dir / relative_out).resolve() try: candidate.relative_to(base_dir) except ValueError: _die(f"Output path escapes output directory: {relative_out}") out_path = candidate ``` For stronger protection against time-of-check/time-of-use and symlink attacks, open the destination using directory-relative operating-system APIs and no-follow semantics where the platform supports them. ]]>
