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

  1. Audit the database: SELECT directory FROM installed_skills and compare each value against sanitize_install_name output
  2. Reinstall the affected skill so the row is rewritten through the normal install path
  3. 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

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


AI-assisted analysis of farion1231/cc-switch@a2e22f3302 (2026-08-16). Data as JSON: /api/errors/b2a9f0f24eab2ef6. Report an issue: GitHub.