T09 · Insecure Skill Coding Practices
- Location
scripts/render.py:1204- Finding
Unrestricted Image Output Path Allows Overwriting Process-Writable Files
- Content
View full analysis
[--image]") print(" --image: 导出为图片,否则输出终端 ASCII") sys.exit(1) mermaid_code = sys.argv[1] output_path = sys.argv[2] mode = sys.argv[3] if len(sys.argv) > 3 else '--terminal' if mode == '--image': result = render_image(mermaid_code, output_path) print(f"图片已保存: {result}") else: result = render_terminal(mermaid_code) print(result) ``` The attacker-controlled path reaches multiple rendering functions, each of which writes directly to that path. A representative sink is: ```python plt.tight_layout() plt.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white', edgecolor='none') plt.close() return output_path ``` Equivalent `savefig(output_path, ...)` sinks occur in every supported image renderer at lines 182, 398, 447, 652, 816, 1017, and 1198. ### Technical Analysis The CLI accepts `sys.argv[2]` as the output path and passes it directly to Matplotlib's `savefig` operation. There is no canonicalization, destination allowlist, extension validation, existing-file check, or symlink check. Consequently, image mode can create or replace any destination writable by the process. Absolute paths and traversal paths such as `../../target` are not rejected. If the selected destination is a symbolic link, the write may follow that link to its target. The generated content is an image rather than arbitrary attacker-selected bytes, but replacing an existing file can still corrupt data or disrupt applications. This issue does not itself grant additional operating-system privileges. Exploitation remain ...[truncated 1528 chars]- Remediation
View remediation
Path: requested = Path(requested_name) if requested.is_absolute() or ".." in requested.parts: raise ValueError("Unsafe output path") destination = (EXPORT_ROOT / requested).resolve() if EXPORT_ROOT not in destination.parents: raise ValueError("Output must remain inside the export directory") if destination.suffix.lower() != ".png": ...[truncated 306 chars]
