{"record":{"id":"4be75e3a6037f7e4","repo":"farion1231/cc-switch","slug":"invalid-skill-directory","errorCode":"INVALID_SKILL_DIRECTORY","errorMessage":"INVALID_SKILL_DIRECTORY","messagePattern":"INVALID_SKILL_DIRECTORY","errorType":"error_code","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-tauri/src/services/skill.rs","lineNumber":776,"sourceCode":"    }\n\n    /// 安装 Skill\n    ///\n    /// 流程：\n    /// 1. 下载到 SSOT 目录\n    /// 2. 保存到数据库\n    /// 3. 同步到启用的应用目录\n    pub async fn install(\n        &self,\n        db: &Arc<Database>,\n        skill: &DiscoverableSkill,\n        current_app: &AppType,\n    ) -> Result<InstalledSkill> {\n        let ssot_dir = Self::get_ssot_dir()?;\n\n        // 允许多级目录（如 a/b/c），但必须是安全的相对路径。\n        let source_rel = Self::sanitize_skill_source_path(&skill.directory).ok_or_else(|| {\n            anyhow!(format_skill_error(\n                \"INVALID_SKILL_DIRECTORY\",\n                &[(\"directory\", &skill.directory)],\n                Some(\"checkZipContent\"),\n            ))\n        })?;\n        // 安装目录名始终使用最后一段，避免在 SSOT 中创建多级目录。\n        let install_name = source_rel\n            .file_name()\n            .and_then(|name| Self::sanitize_install_name(&name.to_string_lossy()))\n            .ok_or_else(|| {\n                anyhow!(format_skill_error(\n                    \"INVALID_SKILL_DIRECTORY\",\n                    &[(\"directory\", &skill.directory)],\n                    Some(\"checkZipContent\"),\n                ))\n            })?;\n\n        // Fast path for an existing installation. The write guard makes the DB","sourceCodeStart":758,"sourceCodeEnd":794,"githubUrl":"https://github.com/farion1231/cc-switch/blob/06082e189d65e6d6dbadc35dacdac1ce6c79d89a/src-tauri/src/services/skill.rs#L758-L794","documentation":"install() first sanitizes DiscoverableSkill.directory with sanitize_skill_source_path, which accepts multi-level relative paths (a/b/c) but rejects anything unsafe. A None return produces INVALID_SKILL_DIRECTORY with suggestion 'checkZipContent' and the offending directory in context. Unsafe here means: not a plain relative path — absolute paths, '..', components that escape the temp root, empty strings, or platform-illegal segments all fail. The value comes from the discovery feed (e.g. skills.sh), where it is only a skillId/relative dir, so a malformed catalog entry or a tampered value triggers it before any download.","triggerScenarios":"Calling install() with skill.directory = \"/etc/skills/x\" (absolute), \"../escape\", \"\" or a path whose segments fail sanitization; a discovery API returning a malformed directory field for that entry.","commonSituations":"Buggy aggregator data with absolute paths or ids containing path-traversal; hand-constructed DiscoverableSkill objects in scripts/tests using OS-specific separators; upstream catalog schema change adding prefixes like './' or Windows backslashes.","solutions":["Pass a safe relative directory like \"python/pdf-tools\" (no leading slash, no '..', forward slashes only).","If the value came from a discovery listing, refresh it — the entry is bad upstream; report/skip that skill.","Validate/normalize directory strings in your own code before calling install (strip leading './' and '/')."],"exampleFix":"// before\nlet skill = DiscoverableSkill { directory: \"/abs/path/pdf-tools\".into(), /* .. */ };\ninstall(&db, &skill, &app).await?; // INVALID_SKILL_DIRECTORY\n\n// after\nlet skill = DiscoverableSkill { directory: \"python/pdf-tools\".into(), /* .. */ };\ninstall(&db, &skill, &app).await?; // proceeds to download","handlingStrategy":"validation","validationCode":"fn safe_rel_dir(dir: &str) -> Option<&str> {\n    if dir.is_empty() || dir.starts_with('/') { return None; }\n    let mut parts = dir.split('/');\n    if dir.split('/').any(|p| p == \"..\" || p == \".\" || p.is_empty()) { return None; }\n    Some(dir)\n}","typeGuard":"fn is_safe_relative_path(dir: &str) -> bool {\n    !dir.is_empty()\n        && !dir.starts_with('/')\n        && !dir.contains('\\\\')\n        && dir.split('/').all(|c| !c.is_empty() && c != \".\" && c != \"..\")\n}","tryCatchPattern":"match install(&db, &skill, &app).await {\n    Err(e) if e.to_string().contains(\"INVALID_SKILL_DIRECTORY\") => {\n        // parse JSON payload; surface context.directory and 'checkZipContent' hint to user\n    }\n    other => other?,\n}","preventionTips":["Normalize discovery feed entries: strip leading './' and '/' before calling install.","Treat directory as untrusted input; never build it into filesystem paths without the relative-path check."],"tags":["skills","path-validation","install","security","tauri"],"backgroundTag":"path-traversal-rejected","analyzedSha":"06082e189d65e6d6dbadc35dacdac1ce6c79d89a","analyzedAt":"2026-08-20T14:29:00.113Z","contentChangedAt":"2026-08-20T14:29:00.113Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}