T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/genai_media.py:264
- Finding
- Path Traversal in Style Loading Enables Unauthorized Local Markdown File Disclosure<![CDATA[ ## Vulnerability Details **File Location**: `scripts/genai_media.py:264-270`, with the vulnerable input flow at `scripts/genai_media.py:292-304` **Vulnerability Type**: Path traversal and unauthorized local file disclosure **Risk Level**: High ### Vulnerable Code ```python def load_style_prompt(style_name: str) -> str: style_file = STYLES_DIR / f"{style_name}.md" if not style_file.exists(): print(f"Style not found, skip: {style_name}") return "" with open(style_file, "r", encoding="utf-8") as f: content = f.read().strip() if not content: print(f"Style is empty, skip: {style_name}") return content ``` The attacker-controlled style name reaches this function through the following code: ```python def resolve_final_prompt(prompt: str | None, styles: list[str] | None, api_key: str) -> str: if styles: selected = styles[:3] loaded = [load_style_prompt(name) for name in selected] loaded = [s for s in loaded if s] if loaded: fused = fuse_style_prompts(loaded, api_key).strip() return fused or DEFAULT_PROMPT print("No valid style content found. Falling back to default prompt.") return DEFAULT_PROMPT return (prompt or "").strip() or DEFAULT_PROMPT ``` ### Technical Analysis The `style_name` argument originates from the repeatable `-s` command-line option and is incorporated directly into a filesystem path: ```python STYLES_DIR / f"{style_name}.md" ``` Although `sanitize_style_name()` exists elsewhere in the script, it is only used when creating style names and is not applied when loading them. The loader does not reject absolute paths, directory separators, or `..` traversal components. It also does not resolve the resulting path and verify that it remains beneath `STYLES_DIR`. Consequently, a value such as `../../private/notes` resolves to a path outside the intended style directory while retaining the automatically a ...[truncated 1988 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Apply strict validation to every style name before performing a lookup. Only permit the same normalized character set used when style files are created: ```python def load_style_prompt(style_name: str) -> str: normalized = sanitize_style_name(style_name) if not normalized or normalized != style_name: raise ValueError("Invalid style name") style_file = (STYLES_DIR / f"{normalized}.md").resolve() styles_root = STYLES_DIR.resolve() try: style_file.relative_to(styles_root) except ValueError: raise ValueError("Style path escapes the styles directory") if not style_file.is_file(): print(f"Style not found, skip: {normalized}") return "" return style_file.read_text(encoding="utf-8").strip() ``` 2. Reject names containing `/`, `\`, `..`, absolute path syntax, null bytes, or any character outside a narrow allowlist such as `[a-z0-9_]`. 3. Resolve and validate the final path with `Path.resolve()` and `Path.relative_to()` before checking or opening it. 4. Use `is_file()` rather than only `exists()` to ensure the target is a regular file. 5. If symbolic links may exist in `styles/`, reject symlinks or ensure the resolved target remains under the resolved style directory. 6. Add tests covering relative traversal, absolute paths, encoded separators, symlinks, empty names, and valid normalized style names. 7. Avoid forwarding file-derived content to an external model unless the file is confirmed to be an authorized style record. ]]>
