T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/run_reminders.py:139
- Finding
- Path Traversal Through an Unvalidated Reminder Identifier## Vulnerability Details **File Location**: `scripts/run_reminders.py`, lines 72–78 and 135–147 **Vulnerability Type**: Path traversal leading to an out-of-scope file write **Risk Level**: Medium ### Vulnerable Code ```python def tts_and_save(text: str, out_path: Path, api_key: str, voice_id: str = DEFAULT_VOICE) -> Path: headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} body = { "model": "SenseAudio-TTS-1.0", "text": text, "stream": False, "voice_setting": {"voice_id": voice_id}, } r = requests.post(TTS_URL, headers=headers, json=body, timeout=30) r.raise_for_status() data = r.json() if data.get("base_resp", {}).get("status_code") != 0: raise RuntimeError(data.get("base_resp", {}).get("status_msg", "TTS failed")) hex_audio = data.get("data", {}).get("audio") if not hex_audio: raise RuntimeError("No audio in response") out_path.parent.mkdir(parents=True, exist_ok=True) with open(out_path, "wb") as f: f.write(bytes.fromhex(hex_audio)) return out_path ``` ```python for r in to_notify: event = r.get("event", "提醒") text = f"提醒:{event}" if dry_run: print(f"[dry-run] would speak: {text}", file=sys.stderr) continue out_path = data_dir / "audio" / f"reminder_{r.get('id', '')}.mp3" try: tts_and_save(text, out_path, api_key, voice_id) play_audio(out_path) except Exception as e: print(f"TTS/play failed for reminder {r.get('id')}: {e}", file=sys.stderr) continue r["status"] = "notified" ``` ### Technical Analysis The reminder identifier is loaded from `reminders.json` and interpolated directly into an output path without validating its characters or checking the resolved destination. Path separators and parent-directory components in the identifier are therefore interpret ...[truncated 1937 chars]
- Remediation
- ## Remediation Suggestions 1. Generate reminder identifiers internally using UUIDs rather than accepting storage-derived identifiers as filenames. 2. Enforce a strict allowlist before using an identifier, such as `[A-Za-z0-9_-]+`, with a reasonable maximum length. 3. Construct and resolve the output path, then verify that it remains beneath the resolved audio directory: ```python import re from uuid import UUID reminder_id = str(r.get("id", "")) if not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", reminder_id): raise ValueError("Invalid reminder ID") audio_dir = (data_dir / "audio").resolve() audio_dir.mkdir(parents=True, exist_ok=True) out_path = (audio_dir / f"reminder_{reminder_id}.mp3").resolve() if out_path.parent != audio_dir: raise ValueError("Output path escapes audio directory") ``` 4. Where supported, use no-follow and exclusive file-opening controls to reduce symlink and overwrite risks. 5. Validate the complete reminder schema when records are created and again when they are loaded from disk. 6. Write reminder data and audio files using restrictive permissions appropriate for potentially sensitive reminder content.
