T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/triage.py:135
- Finding
- Path Traversal in Symptom History File Handling<![CDATA[ ## Vulnerability Details **File Location**: `scripts/triage.py:135-154` **Vulnerability Type**: Path traversal and arbitrary JSON file access **Risk Level**: High ### Vulnerable Code ```python class SymptomHistoryManager: def __init__(self, user_id: str): self.user_id = user_id self.history_dir = os.path.expanduser("~/.openclaw/symptom_history") self.history_file = os.path.join(self.history_dir, f"{user_id}.json") os.makedirs(self.history_dir, exist_ok=True) def save_assessment(self, assessment: Dict[str, Any]): history = self.load_history() history.append({ 'timestamp': datetime.now().isoformat(), 'assessment': assessment }) with open(self.history_file, 'w', encoding='utf-8') as f: json.dump(history[-50:], f, ensure_ascii=False, indent=2) def load_history(self) -> List[Dict[str, Any]]: if os.path.exists(self.history_file): try: with open(self.history_file, 'r', encoding='utf-8') as f: return json.load(f) except: pass return [] ``` ### Technical Analysis The caller-controlled `user_id` is inserted directly into a filesystem path without validation, normalization, or containment checking. A value containing traversal components such as `../` can cause the resolved history path to escape `~/.openclaw/symptom_history`. A value beginning with an absolute path can also cause `os.path.join()` to discard the intended base directory. The `.json` suffix limits target names but does not prevent access to JSON configuration or data files. The `--history` command exposes the contents of a selected JSON file through `load_history()`. During an assessment, `save_assessment()` can overwrite a selected file if the existing JSON is compatible with the expected list structure. ### Attack Path 1. An attacker who can invoke the CLI or API supplies a cr ...[truncated 1041 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Do not use a raw external identifier as a filename. Derive a fixed-length filename using a keyed hash or a cryptographic digest. 2. If identifiers must remain readable, enforce a strict allowlist such as `^[A-Za-z0-9_-]{1,64}$`. 3. Reject absolute paths, path separators, `.` components, and `..` components. 4. Resolve both the base directory and candidate path, then verify containment: ```python from pathlib import Path import hashlib base = Path("~/.openclaw/symptom_history").expanduser().resolve() safe_id = hashlib.sha256(user_id.encode("utf-8")).hexdigest() candidate = (base / f"{safe_id}.json").resolve() if base not in candidate.parents: raise ValueError("Invalid user identifier") ``` 5. Open files using restrictive permissions and avoid following symbolic links where supported. 6. Add tests covering `../`, nested traversal, absolute paths, path separators, symlinks, empty identifiers, and unusually long identifiers. ]]>
