T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/parse.py:54
- Finding
- Path Traversal Through Unsanitized Keyword-Based Filename<![CDATA[ ## Vulnerability Details **File Location**: `scripts/parse.py`, lines 54-55; file-write sink at lines 120-121 **Vulnerability Type**: Path traversal and arbitrary file overwrite **Risk Level**: Medium ### Vulnerable Code ```python md_filename = f"{keyword.replace(' ', '_')}_raw_data.md" md_path = os.path.join(output_dir, md_filename) ``` The constructed path is later used directly as a file-write destination: ```python with open(md_path, 'w', encoding='utf-8') as f: f.write(output_md) ``` ### Technical Analysis The output filename is derived from the user-controlled `keyword`. The only transformation replaces spaces with underscores; path separators, parent-directory sequences such as `../`, absolute path components, and platform-specific separators are not rejected. `os.path.join()` does not guarantee that the resulting path remains inside `output_dir`. A keyword containing traversal components can therefore cause `md_path` to resolve outside the intended output directory. If an absolute keyword is accepted, normal `os.path.join()` semantics can also discard the preceding output directory. Although the generated filename always receives the `_raw_data.md` suffix, an attacker can still target writable Markdown paths or create files in unintended directories. ### Attack Path 1. An attacker or untrusted caller invokes `run.sh` with a crafted keyword such as `../../target`. 2. `run.sh` passes that keyword unchanged to `parse.py`. 3. `parse.py` transforms it into `../../target_raw_data.md`. 4. `os.path.join(output_dir, md_filename)` constructs a path that escapes `output_dir`. 5. The final `open(..., 'w')` creates or truncates the escaped destination using the privileges of the process. ### Impact Assessment Successful exploitation permits creation or overwrite of `.md` files in any filesystem location writable by the Skill process. The vulnerability does not independently elevate privileges, but it acts with all existing filesystem privile ...[truncated 200 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions - Generate the output filename from a strict allowlist, such as ASCII letters, digits, underscores, and hyphens. - Explicitly reject `/`, `\`, `..`, NUL characters, and absolute paths. - Resolve both the output directory and candidate destination with `os.path.realpath()`. - Verify with `os.path.commonpath()` that the resolved destination remains beneath the resolved output directory. - Consider using an application-generated identifier for the physical filename and retaining the original keyword only as document metadata. - Where overwriting is unnecessary, open the destination using exclusive creation mode (`'x'`) to prevent accidental truncation. Example hardening: ```python safe_keyword = re.sub(r"[^A-Za-z0-9_-]+", "_", keyword).strip("_") if not safe_keyword: safe_keyword = "search" base_dir = os.path.realpath(output_dir) md_path = os.path.realpath( os.path.join(base_dir, f"{safe_keyword}_raw_data.md") ) if os.path.commonpath([base_dir, md_path]) != base_dir: raise ValueError("Output path escapes the configured output directory") ``` ]]>
