T09 · Insecure Skill Coding Practices
- Location
scripts/deep_skill_install.py:290- Finding
Unvalidated Skill Name Allows Writes Outside the Selected Installation Directory
- Content
View full analysis
Vulnerability Details
File Location:
scripts/deep_skill_install.py, lines 290–326
Vulnerability Type: Path traversal leading to arbitrary file write
Risk Level: HighVulnerable Code
python def install_skill(skill_name: str, target_dir: Path, agent_type: str = None) -> bool: """安装 skill 主流程。target_dir 为当前 Agent 的 skills 目录。""" # 内置 skill 不允许通过脚本覆盖 if _is_builtin_skill(skill_name): print(f"✗ '{skill_name}' 是内置 skill,由桌面端自动管理,无需安装", file=sys.stderr) return False target_dir.mkdir(parents=True, exist_ok=True) print(f" 安装 skill '{skill_name}' 到 {target_dir}...") # 获取元数据(用于 version 和 skillMdContent 兜底) metadata = get_skill_metadata(skill_name) version = _get_version(metadata) skill_dir = target_dir / skill_name skill_dir.mkdir(parents=True, exist_ok=True) success = False # 下载 zip 包(包含完整文件如 scripts/);非 zip 则当作单文件 SKILL.md try: print(f" 下载 zip 包 (version={version})...", file=sys.stderr) data = download_skill_zip( skill_name, version if version != "unknown" else None, agent_type=agent_type, ) if data[:4] == b'PK\x03\x04': success = install_from_zip(data, skill_name, target_dir) else: print(" ⚠ 下载内容非 zip 格式,保存为 SKILL.md", file=sys.stderr) (skill_dir / "SKILL.md").write_bytes(data) success = True except Exception as e: print(f" ⚠ 下载失败: {e},回退到 skillMdContent", file=sys.stderr) # 回退:使用 /skills/{name} 返回的 skillMdContent(仅 SKILL.md,不含 scripts) if not success: skill_md_content = metadata.get("skillMdContent", "") if skill_md_content: (skill_dir / "SKILL.md").write_text( skill_md_content, encoding="utf-8", ) success = TrueTechnical Analysis
`skill_na ...[truncated 3182 chars]
- Remediation
View remediation
Remediation Suggestions
-
Validate
skill_namebefore_is_builtin_skill(), directory creation, network access, or metadata processing. Use a strict slug allowlist consistent with the uninstall path:python SKILL_NAME_RE = re.compile(r"^[A-Za-z0-9_-]+$") if not SKILL_NAME_RE.fullmatch(skill_name): raise ValueError("Invalid skill name") -
Reject absolute paths and special path components explicitly:
python candidate = Path(skill_name) if candidate.is_absolute() or any(part in {".", ".."} for part in candidate.parts): raise ValueError("Invalid skill path") -
Resolve and enforce containment before creating or writing anything:
python target_root = target_dir.expanduser().resolve() skill_dir = (target_root / skill_name).resolve() try: skill_dir.relative_to(target_root) except ValueError: raise ValueError("Skill destination escapes target directory") -
Use
Path.relative_to()oros.path.commonpath()for archive-output containment checks. Do not use string-prefix comparisons, because sibling paths can share the same textual prefix. -
Apply the same validated destination to direct
SKILL.mdwrites, metadata fallback writes, archive extraction, and.meyo/origin.jsoncreation. -
Add regression tests for
../escape, nested traversal, absolute POSIX paths, Windows drive paths, UNC paths, separators, and names resembling a valid directory prefix.
-
