T09 · Insecure Skill Coding Practices
Error
- Location
- chat_archive.py:205
- Finding
- Path Traversal Through Unvalidated Conversation Identifier<![CDATA[ ## Vulnerability Details **File Location**: `chat_archive.py`, lines 205–207 **Vulnerability Type**: Path traversal and arbitrary file write **Risk Level**: High ### Vulnerable Code ```python safe_id = conv['id'] with open(self.output_dir / f'{safe_id}.html', 'w', encoding='utf-8') as f: f.write(html_content) ``` ### Technical Analysis Conversation records are loaded from JSON files under the archive directory. The attacker-controlled `id` field is subsequently used as part of an output path without validation or normalization. Although the variable is named `safe_id`, no sanitization is performed. A conversation identifier containing parent-directory components, such as `../../public/page`, causes the resulting path to escape `self.output_dir`. An absolute identifier can also cause `pathlib` path composition to discard the intended parent directory. The forced `.html` suffix limits the filename extension but does not prevent writing outside the archive directory. ### Attack Path 1. An attacker supplies a crafted conversation export or causes a malicious JSON file to be placed in `chat-archive/json`. 2. The JSON conversation contains an identifier such as: ```json { "id": "../../public/attacker-page", "title": "Crafted conversation", "source": "chatgpt", "messages": [] } ``` 3. The victim runs: ```bash python3 chat_archive.py ``` 4. `_load_conversations()` accepts the JSON record without schema validation. 5. `_generate_conversation_page()` constructs the path from the malicious identifier. 6. The generated HTML is written outside `chat-archive/web`, provided the process has filesystem permission to write to the target location. ### Impact Assessment The vulnerability allows creation or overwrite of `.html` files anywhere writable by the current operating-system user. It does not directly grant privileges beyond those of the process, but it can: - Overwrite user-owned HTML pages. - Modify co ...[truncated 332 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions Do not use imported identifiers directly as filesystem names. 1. Normalize identifiers using a strict allowlist: ```python raw_id = str(conv.get('id', '')) safe_id = re.sub(r'[^a-zA-Z0-9_-]', '-', raw_id).strip('-_') if not safe_id: raise ValueError("Conversation ID does not produce a valid filename") ``` 2. Resolve the destination and verify containment: ```python output_root = self.output_dir.resolve() output_path = (output_root / f'{safe_id}.html').resolve() if output_root not in output_path.parents: raise ValueError("Conversation output path escapes archive directory") with open(output_path, 'w', encoding='utf-8') as f: f.write(html_content) ``` 3. Prefer generating filenames from a cryptographic digest or an application-generated identifier rather than trusting imported metadata. 4. Apply the same filename mapping when creating links in `index.html`, so page generation and navigation use one validated identifier. 5. Add tests covering `../`, absolute paths, Windows separators, empty identifiers, Unicode separators, and identifiers containing only rejected characters. ]]>
