T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/lib/notifiers.py:217
- Finding
- Path Traversal in Discord Notification Audit-File Creation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/lib/notifiers.py:217-228` **Vulnerability Type**: Path traversal and arbitrary file creation **Risk Level**: Medium ### Vulnerable Code ```python def _write_notification_file(self, task: CallbackTask, message: str) -> None: """Write notification to file as audit trail.""" notification_file = os.path.join( self.output_dir, task.task_id + "_" + task.updated_at.replace(':', '-') + ".json" ) notification_data = { 'task_id': task.task_id, 'channel': self.channel, 'reply_to': task.reply_to, 'message': message, 'timestamp': task.updated_at, } with open(notification_file, 'w', encoding='utf-8') as f: json.dump(notification_data, f, ensure_ascii=False, indent=2) ``` ### Technical Analysis The filename is constructed using `task.task_id` without validating or sanitizing path separators, absolute paths, `..` components, or symlink traversal. The task ID can originate from the `--task-id` argument in `scripts/register_task.py` or from a record inserted directly into the shared JSONL task store. Because `os.path.join()` does not guarantee that the resulting path remains under `self.output_dir`, a crafted task ID can escape the notification directory. An absolute task ID causes the output directory to be discarded, while traversal components can target writable parent directories. The watcher subsequently opens the computed destination in write mode, creating the file or truncating an existing file whose complete generated name can be predicted. The timestamp suffix limits direct selection of an arbitrary existing filename, but it does not prevent unauthorized file creation outside the intended directory. Existing directories must be present, and the effective impact remains constrained by the operating-system permissions of the watcher process. ### Attack Path 1. An attacker obtains the ability to register a task or ...[truncated 1324 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Validate task IDs at every ingestion boundary using a strict allowlist, for example: ```python import re if not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", task.task_id): raise ValueError("Invalid task ID") ``` 2. Apply the same validation in `register_task.py`, `CallbackTask.__post_init__()`, and when deserializing records from the shared JSONL store. 3. Canonicalize and enforce the destination boundary before opening the file: ```python base = os.path.realpath(self.output_dir) destination = os.path.realpath(os.path.join(base, filename)) if os.path.commonpath([base, destination]) != base: raise ValueError("Notification path escapes output directory") ``` 4. Generate filenames independently of user-controlled identifiers, such as with a UUID, and store the original task ID only inside the JSON document. 5. Use atomic, exclusive file creation and reject symlinks where supported. Open files with restrictive permissions and avoid silently overwriting existing files. 6. Run the watcher as a dedicated, unprivileged account with write access limited to its task, audit, and notification directories. ]]>
