T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/batch_search.py:152
- Finding
- Batch Query Names Permit Arbitrary File Writes Through Path Traversal<![CDATA[ ## Vulnerability Details **File Location**: `scripts/batch_search.py:152, 221-222` **Vulnerability Type**: Path traversal and arbitrary file overwrite **Risk Level**: High ### Vulnerable Code ```python query_name = query_spec.get("name", f"query_{index:03d}") ``` ```python if save_individual: output_file = self.output_dir / f"{result['query_name']}.json" save_json(result["results"], output_file) ``` The invoked helper also creates parent directories and opens the destination in overwrite mode: ```python def save_json(data: Any, filepath: str, indent: int = 2, ensure_ascii: bool = False) -> None: filepath = Path(filepath) filepath.parent.mkdir(parents=True, exist_ok=True) with open(filepath, "w", encoding="utf-8") as f: json.dump(data, f, indent=indent, ensure_ascii=ensure_ascii, default=str) ``` ### Technical Analysis The `name` property comes directly from an input JSONL query specification. It is used as part of an output path without rejecting absolute paths, path separators, or `..` components. `pathlib.Path` does not automatically restrict a joined path to its intended parent directory. For example, a name such as `../../outside/result` produces: ```text <output_dir>/../../outside/result.json ``` The operating system resolves the traversal components before the file is written. In addition, `save_json()` creates missing parent directories and opens existing files with mode `"w"`, allowing them to be truncated and replaced. ### Attack Path 1. An attacker supplies or modifies a batch-search JSONL file. 2. The attacker sets the query name to a traversal value, such as: ```json {"query":"machine learning","name":"../../target"} ``` 3. The victim runs: ```bash python scripts/batch_search.py --input malicious.jsonl --output output/metadata ``` 4. The search succeeds, preserving the malicious `query_name`. 5. The program constructs `output/metadata/../../target.json`. 6. `save_json()` resolve ...[truncated 576 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions - Do not use the raw `name` field as a filesystem path. - Restrict generated names to a conservative allowlist such as letters, digits, underscores, and hyphens. - Reject empty names, absolute paths, path separators, drive prefixes, and `..` components. - Resolve the final destination and verify that it remains below the resolved output directory. - Consider using exclusive creation when overwriting existing results is not explicitly requested. Example hardening: ```python import re from pathlib import Path def safe_output_path(output_dir: Path, query_name: str) -> Path: if not isinstance(query_name, str): raise ValueError("Query name must be a string") safe_name = re.sub(r"[^A-Za-z0-9_-]", "_", query_name).strip("_") if not safe_name: raise ValueError("Query name does not contain a valid filename") base = output_dir.resolve() destination = (base / f"{safe_name}.json").resolve() if destination.parent != base: raise ValueError("Output path escapes the configured directory") return destination ``` Apply this validation before every individual-result write. ]]>
