{"record":{"id":"4bc64f9882aa1668","repo":"BigPizzaV3/CodexPlusPlus","slug":"skill-4bc64f","errorCode":null,"errorMessage":"skill 中不允许包含符号链接：{}","messagePattern":"skill 中不允许包含符号链接：(.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/skills.rs","lineNumber":788,"sourceCode":"    zip_bytes: &[u8],\n    repo_path: &str,\n    destination: &Path,\n) -> anyhow::Result<()> {\n    let repo_path = repo_path.trim_matches('/');\n    if repo_path.is_empty() {\n        anyhow::bail!(\"skill 在仓库中的路径不能为空\");\n    }\n    let prefix = format!(\"{repo_path}/\");\n    let mut archive =\n        zip::ZipArchive::new(Cursor::new(zip_bytes)).context(\"skill 仓库压缩包无法解析\")?;\n    let mut wrote_manifest = false;\n\n    for index in 0..archive.len() {\n        let mut file = archive\n            .by_index(index)\n            .with_context(|| format!(\"读取压缩包条目 {index} 失败\"))?;\n        if file.is_symlink() {\n            anyhow::bail!(\"skill 中不允许包含符号链接：{}\", file.name());\n        }\n        let Some(relative) = crate::plugin_marketplace::zip_entry_relative_path(file.name()) else {\n            continue;\n        };\n        // zip 内部的分隔符恒为 '/'，但上面拿回来的是 PathBuf，在 Windows 上\n        // to_str() 会渲染成 '\\'，跟用 '/' 拼出来的 prefix 永远匹配不上——结果就是\n        // 一个文件都解不出来，报「没有 SKILL.md」。这里统一拼回 '/' 再比。\n        let relative = relative\n            .components()\n            .filter_map(|component| component.as_os_str().to_str())\n            .collect::<Vec<_>>()\n            .join(\"/\");\n        let Some(inner) = relative.strip_prefix(prefix.as_str()) else {\n            continue;\n        };\n        if inner.is_empty() {\n            continue;\n        }","sourceCodeStart":770,"sourceCodeEnd":806,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/skills.rs#L770-L806","documentation":"While iterating zip entries, extract_skill_subtree checks file.is_symlink() and bails naming the entry. Symlink entries could point outside the destination on extraction — the same policy as codex's built-in skill-installer — so any archive containing one is refused entirely.","triggerScenarios":"从 GitHub 仓库（或任意来源）下载的 zip 中包含 symlink 条目，extract_skill_subtree 或 install_from_zip 解压该包时触发。","commonSituations":"仓库里作者为共享文件创建了 symlink（macOS/Linux 常见）；构建产物打包时把链接原样打进 zip；恶意包注入 symlink 实施攻击。","solutions":["改用不含 symlink 的 zip 包重新安装（联系仓库作者移除 symlink 或用真实文件替代）","若自有仓库，把 symlink 替换为复制出来的真实文件后重新打包","从可信来源下载 skill 包，避免安装未知第三方仓库的 skill"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match state.install_from_zip(&zip_bytes, &repo_path) {\n    Err(e) if e.to_string().contains(\"符号链接\") => {\n        eprintln!(\"该 skill 包含 symlink，已拒绝安装，请改用无 symlink 的包\");\n    }\n    other => other?,\n}","preventionTips":["只安装可信来源的 skill 包","打包 skill 仓库时用 cp -L 等方式把 symlink 展开为真实文件","安装失败的 zip 不要重试，先检查包内容（unzip -l 查看类型）"],"tags":["security","zip","symlink","path-traversal"],"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"}