T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/mlx-audio.py:501
- Finding
- Path Traversal and Arbitrary File Overwrite Through Voice Profile ID## Vulnerability Details **File Location**: `scripts/mlx-audio.py:501-543` **Vulnerability Type**: Unsanitized path construction and arbitrary file overwrite **Risk Level**: High ### Vulnerable Code ```python voice_id = args.id or str(uuid.uuid4())[:8] voice_dir = os.path.join(voices_dir, voice_id) os.makedirs(voice_dir, exist_ok=True) # Normalize text: replace newlines with spaces text = _normalize_text(args.text) instruct = _normalize_text(args.instruct) if args.instruct else None if not instruct: raise ValueError("--instruct 参数是必需的,请提供语音风格描述") # Generate audio using TTS output_audio = os.path.join(voice_dir, "ref_audio.wav") # Run TTS to generate the audio (always use VoiceDesign model for voice creation) _ensure_mlx_audio() from mlx_audio.tts.utils import load_model as load_tts_model model = load_tts_model(DEFAULT_VOICEDESIGN_MODEL) kwargs = { "text": text, "language": args.language, "instruct": instruct, } results = list(model.generate(**kwargs)) if not results: raise RuntimeError("TTS 生成失败:未返回音频结果") result = results[0] sample_rate = _get_sample_rate(result, model) audio = result.audio sf.write(output_audio, audio, sample_rate) # Save reference text ref_text_path = os.path.join(voice_dir, "ref_text.txt") with open(ref_text_path, "w", encoding="utf-8") as f: f.write(text) # Save instruct instruct_path = os.path.join(voice_dir, "ref_instruct.txt") with open(instruct_path, "w", encoding="utf-8") as f: f.write(instruct) ``` ### Technical Analysis The user-controlled `--id` argument is used directly as a path component without validation or canonical containment checking. Python's `os.path.join()` does not guarantee that the resulting path remains under `voices_dir`: - An absolute `voice_id` causes the preceding `voices_dir` component to be discarded. - A relative value containing `../` can traverse outside the intended `voices/` directory. - Existing directories are accepted because `os.makedirs(..., exist_ok ...[truncated 1907 chars]
- Remediation
- ## Remediation Suggestions 1. Restrict voice IDs to a conservative allowlist, for example: ```python import re VOICE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{1,64}$") if not VOICE_ID_PATTERN.fullmatch(voice_id): raise ValueError( "Voice ID may contain only letters, digits, underscores, and hyphens" ) ``` 2. Canonicalize the base and candidate paths and enforce containment: ```python voices_dir = os.path.realpath(get_voices_dir()) voice_dir = os.path.realpath(os.path.join(voices_dir, voice_id)) if os.path.commonpath([voices_dir, voice_dir]) != voices_dir: raise ValueError("Voice path escapes the voices directory") ``` 3. Explicitly reject absolute paths, path separators, `.` components, and `..` components. 4. Apply the same validation and containment checks to `get_voice_path()`. 5. Refuse to overwrite existing profiles by default. Require an explicit, documented replacement option when replacement is intended. 6. Consider using exclusive file creation and restrictive filesystem permissions for stored voice data. 7. Add tests covering absolute paths, `../` traversal, nested paths, symlink-based escapes, and existing-profile replacement.
