farion1231/cc-switch · error · anyhow::Error
Invalid skill directory (possible path traversal): {director
Error message
Invalid skill directory (possible path traversal): {directory:?} What it means
Security guard in require_valid_directory: a stored skill `directory` value must be a canonical single path segment. It re-runs sanitize_install_name and requires the normalized result to equal the raw value byte-for-byte; any difference (leading/trailing whitespace, separators, dot-prefix, empty) is rejected as a possible path traversal. The doc comment explains two entrances bypass install-time validation — remote snapshots written via raw SQL and untrusted meta.json — so every join-into-filesystem-path site (especially remove_dir_all) must call this first.
Source
Thrown at src-tauri/src/services/skill.rs:2835
}
_ => None,
}
}
/// 校验来自 DB 行 / 备份 meta.json 等外部来源的 directory 字段。
///
/// 存储值按构造本应是单段安装名(见 sanitize_install_name),但有两个入口
/// 会绕过安装期校验:同步导入的远端快照直接灌库(raw SQL),以及手工放置 /
/// 不可信备份里的 meta.json。任何把它 join 进文件系统路径的使用点(尤其是
/// remove_dir_all 这类删除操作)必须先过这道校验,拒绝路径穿越。
///
/// 只校验、不归一化:`sanitize_install_name` 会 `trim()`,若拿它的返回值替换
/// 原值,磁盘上真实带空格的目录名就再也 join 不中。所以这里要求归一化结果与
/// 原值逐字相同,否则一律视为非法。
fn require_valid_directory(directory: &str) -> Result<String> {
match Self::sanitize_install_name(directory) {
Some(normalized) if normalized == directory => Ok(normalized),
_ => Err(anyhow!(
"Invalid skill directory (possible path traversal): {directory:?}"
)),
}
}
/// GitHub 账号名(user / org login)。
///
/// 只放行 ASCII 字母数字与 `-`。这比 GitHub 自身的规则更严,但该字段会被拼进
/// 下载 URL,任何 `/`、`.`、`%`、`\` 都可能改写请求落点(见 validate_repo_ref)。
fn is_valid_github_owner(owner: &str) -> bool {
!owner.is_empty()
&& owner.len() <= 39
&& owner.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
}
/// GitHub 仓库名。允许 `.` `-` `_`,但整体不能是 `.` 或 `..`。
fn is_valid_github_repo_name(name: &str) -> bool {
!name.is_empty()View on GitHub (pinned to a2e22f3302)
Solutions
- Audit the database: SELECT directory FROM installed_skills and compare each value against sanitize_install_name output
- Reinstall the affected skill so the row is rewritten through the normal install path
- Delete the offending row/directory if the skill is unknown; do not 'fix' it by renaming with whitespace kept on disk (the guard intentionally refuses normalization because the real on-disk name may contain spaces)
Example fix
// before — trusting a DB row before a filesystem delete let path = ssot_dir.join(&row.directory); fs::remove_dir_all(&path)?; // after — validate before any join let directory = SkillService::require_valid_directory(&row.directory)?; let path = ssot_dir.join(&directory); fs::remove_dir_all(&path)?;
Defensive patterns
Strategy: type-guard
Validate before calling
// Rust — audit DB values before any filesystem operation on them
for dir in db.get_all_installed_skills()?.keys() {
if SkillService::sanitize_install_name(dir).map_or(true, |n| n != dir) {
log::warn!("polluted directory value in DB: {dir:?}");
}
} Type guard
// Rust predicate mirroring the guard, for callers that hold raw values
fn is_valid_skill_directory(raw: &str) -> bool {
let t = raw.trim();
!t.is_empty()
&& !t.contains('/')
&& !t.contains('\\')
&& !t.starts_with('.')
&& t != "." && t != ".."
&& t == raw // no normalization delta allowed
} Try / catch
match SkillService::require_valid_directory(&row.directory) {
Ok(dir) => { /* safe to join into paths */ }
Err(_) => { /* drop the row / quarantine the skill; never fall back to raw value */ }
} Prevention
- Never write directory values into the DB via raw SQL; only through the install path that runs sanitize_install_name
- Validate directory strings the moment they enter from remote snapshots or meta.json files
- On error, do not 'repair' by using the sanitized value as the new name — on-disk names may legitimately contain spaces
When it happens
Trigger: A DB row or meta.json carrying a directory like " my-skill" (whitespace, normalization differs), "../escape", "a/b", "a\\b", ".hidden", or "..". Typically introduced by a polluted WebDAV-synced remote snapshot or a hand-crafted backup, then tripped when sync, delete, or uninstall joins the value into a path.
Common situations: Multi-device sync importing a snapshot written by an older/buggy version; restoring a backup edited by hand; tampered skill packages. The error is a defense firing, not a bug in the caller.
Related errors
- Invalid backup id: {backup_id}
- Unsupported URL scheme
- INVALID_SKILL_DIRECTORY
- INVALID_REPO_REF
- NO_SKILLS_IN_ZIP
AI-assisted analysis of farion1231/cc-switch@a2e22f3302 (2026-08-16).
Data as JSON: /api/errors/b2a9f0f24eab2ef6.
Report an issue: GitHub.