{"record":{"id":"7935a110026e5280","repo":"BigPizzaV3/CodexPlusPlus","slug":"skill-id-id","errorCode":null,"errorMessage":"非法的 skill id：{id}","messagePattern":"非法的 skill id：(.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/skills.rs","lineNumber":625,"sourceCode":"        subdir,\n        enabled: repo.enabled,\n    })\n}\n\nfn is_safe_repo_segment(value: &str) -> bool {\n    !value.is_empty()\n        && value\n            .chars()\n            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))\n}\n\n/// skill id 直接参与拼路径，必须挡住 `..` 和分隔符。\nfn validate_skill_id(id: &str) -> anyhow::Result<()> {\n    if id.is_empty() {\n        anyhow::bail!(\"skill id 不能为空\");\n    }\n    if id == \".\" || id == \"..\" || id.contains('/') || id.contains('\\\\') {\n        anyhow::bail!(\"非法的 skill id：{id}\");\n    }\n    Ok(())\n}\n\n/// GitHub trees API 的响应 → 该仓库里的 skill 清单。\n///\n/// 一个目录只要直接含 `SKILL.md` 就算一个 skill；`content_hash` 用子树里\n/// 每个文件的 blob sha 算，远端内容一变哈希就变，不用下载就能判断有没有更新。\npub fn parse_skills_from_tree(repo: &SkillRepo, tree: &Value) -> Vec<RemoteSkill> {\n    let Some(items) = tree.get(\"tree\").and_then(Value::as_array) else {\n        return Vec::new();\n    };\n    let prefix = if repo.subdir.is_empty() {\n        String::new()\n    } else {\n        format!(\"{}/\", repo.subdir)\n    };\n    let repo_key = repo.key();","sourceCodeStart":607,"sourceCodeEnd":643,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/skills.rs#L607-L643","documentation":"The second half of validate_skill_id rejects ids equal to '.', '..', or containing '/' or '\\\\'. Because the id is joined into SSOT/backup paths, these values would cause path traversal or self-reference; the message includes the offending id.","triggerScenarios":"任一接受 skill_id 的公开方法传入 \"..\"、\".\"、\"a/b\"、\"a\\\\b\" 等含分隔符或相对路径段的字符串。","commonSituations":"调用方把仓库内相对路径（如 \"skills/my-skill\"）当作 skill id 传入 uninstall；用户在输入框中输入路径而非纯 id；从外部存储读回的 id 带有历史遗留分隔符。","solutions":["传入的 skill_id 必须是单一路径段：不含 / 和 \\ 且不为 . 或 ..","如果手头是仓库内路径，先取文件名或最后一段作为 skill id","对用户输入先做白名单校验 ^[A-Za-z0-9._-]+$ 且排除 . / .."],"exampleFix":"// before\nstate.uninstall(&user_input)?; // user_input 可能是 \"..\"\n// after\nfn is_valid_skill_id(id: &str) -> bool {\n    !id.is_empty() && id != \".\" && id != \"..\"\n        && !id.contains('/') && !id.contains('\\\\')\n}\nif is_valid_skill_id(&user_input) {\n    state.uninstall(&user_input)?;\n}","handlingStrategy":"validation","validationCode":"fn is_valid_skill_id(id: &str) -> bool {\n    !id.is_empty()\n        && id != \".\" && id != \"..\"\n        && !id.contains('/') && !id.contains('\\\\')\n        && id.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))\n}","typeGuard":null,"tryCatchPattern":"match state.uninstall(&skill_id) {\n    Err(e) if e.to_string().contains(\"非法的 skill id\") => eprintln!(\"非法 id，拒绝操作：{}\", skill_id),\n    other => other?,\n}","preventionTips":["只用 skill 清单返回的 id 字段，不要把仓库相对路径当 id","对用户输入的 id 做白名单正则校验","从持久化存储读回 id 后仍重新校验，防止数据被篡改"],"tags":["validation","path-traversal","security","skills"],"backgroundTag":"path-traversal-blocked","analyzedSha":"b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6","analyzedAt":"2026-09-19T23:35:21.129Z","contentChangedAt":"2026-09-19T23:35:21.129Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}