BigPizzaV3/CodexPlusPlus · error · anyhow::Error
非法的 skill id:
Error message
非法的 skill id:{id} What it means
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.
Solutions
- 传入的 skill_id 必须是单一路径段:不含 / 和 \ 且不为 . 或 ..
- 如果手头是仓库内路径,先取文件名或最后一段作为 skill id
- 对用户输入先做白名单校验 ^[A-Za-z0-9._-]+$ 且排除 . / ..
Example fix
// before
state.uninstall(&user_input)?; // user_input 可能是 ".."
// after
fn is_valid_skill_id(id: &str) -> bool {
!id.is_empty() && id != "." && id != ".."
&& !id.contains('/') && !id.contains('\\')
}
if is_valid_skill_id(&user_input) {
state.uninstall(&user_input)?;
} Defensive patterns
Strategy: validation
Validate before calling
fn is_valid_skill_id(id: &str) -> bool {
!id.is_empty()
&& id != "." && id != ".."
&& !id.contains('/') && !id.contains('\\')
&& id.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
} Try / catch
match state.uninstall(&skill_id) {
Err(e) if e.to_string().contains("非法的 skill id") => eprintln!("非法 id,拒绝操作:{}", skill_id),
other => other?,
} Prevention
- 只用 skill 清单返回的 id 字段,不要把仓库相对路径当 id
- 对用户输入的 id 做白名单正则校验
- 从持久化存储读回 id 后仍重新校验,防止数据被篡改
When it happens
Trigger: 任一接受 skill_id 的公开方法传入 ".."、"."、"a/b"、"a\\b" 等含分隔符或相对路径段的字符串。
Common situations: 调用方把仓库内相对路径(如 "skills/my-skill")当作 skill id 传入 uninstall;用户在输入框中输入路径而非纯 id;从外部存储读回的 id 带有历史遗留分隔符。
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- API 地址须使用 HTTPS,且不含账号、密码、查询参数或片段
- API 地址须使用 HTTPS,且不含账号、密码、查询参数或片段
- CDP WebSocket host must be loopback
- 主题市场资源地址越界
- invalid Dream Skin theme id
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19).
Data as JSON: /api/errors/7935a110026e5280.
Report an issue: GitHub.
Appendix: source
Thrown at crates/codex-plus-core/src/skills.rs:625
subdir,
enabled: repo.enabled,
})
}
fn is_safe_repo_segment(value: &str) -> bool {
!value.is_empty()
&& value
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
}
/// skill id 直接参与拼路径,必须挡住 `..` 和分隔符。
fn validate_skill_id(id: &str) -> anyhow::Result<()> {
if id.is_empty() {
anyhow::bail!("skill id 不能为空");
}
if id == "." || id == ".." || id.contains('/') || id.contains('\\') {
anyhow::bail!("非法的 skill id:{id}");
}
Ok(())
}
/// GitHub trees API 的响应 → 该仓库里的 skill 清单。
///
/// 一个目录只要直接含 `SKILL.md` 就算一个 skill;`content_hash` 用子树里
/// 每个文件的 blob sha 算,远端内容一变哈希就变,不用下载就能判断有没有更新。
pub fn parse_skills_from_tree(repo: &SkillRepo, tree: &Value) -> Vec<RemoteSkill> {
let Some(items) = tree.get("tree").and_then(Value::as_array) else {
return Vec::new();
};
let prefix = if repo.subdir.is_empty() {
String::new()
} else {
format!("{}/", repo.subdir)
};
let repo_key = repo.key();View on GitHub (pinned to b1ed92e5e4)