farion1231/cc-switch · error

Invalid backup id: {backup_id}

Error message

Invalid backup id: {backup_id}

What it means

Path-traversal guard in backup_path_for_id: a backup id is rejected if it contains '..', '/', '\\', or is blank after trimming. Backup ids are used to build a path under the backup directory for restore/delete operations, so the check prevents crafted ids from escaping that directory.

Source

Thrown at src-tauri/src/services/skill.rs:3465

        }

        entries.sort_by_key(|(_, modified)| *modified);
        let remove_count = entries.len().saturating_sub(SKILL_BACKUP_RETAIN_COUNT);

        for (path, _) in entries.into_iter().take(remove_count) {
            fs::remove_dir_all(&path)?;
        }

        Ok(())
    }

    fn backup_path_for_id(backup_id: &str) -> Result<PathBuf> {
        if backup_id.contains("..")
            || backup_id.contains('/')
            || backup_id.contains('\\')
            || backup_id.trim().is_empty()
        {
            return Err(anyhow!("Invalid backup id: {backup_id}"));
        }

        Ok(Self::get_backup_dir()?.join(backup_id))
    }

    fn read_backup_metadata(backup_path: &Path) -> Result<SkillBackupMetadata> {
        let metadata_path = backup_path.join("meta.json");
        let content = fs::read_to_string(&metadata_path)
            .with_context(|| format!("failed to read {}", metadata_path.display()))?;
        serde_json::from_str(&content)
            .with_context(|| format!("failed to parse {}", metadata_path.display()))
    }

    fn create_uninstall_backup(skill: &InstalledSkill) -> Result<Option<PathBuf>> {
        Self::create_uninstall_backup_excluding(skill, None)
    }

    fn create_uninstall_backup_excluding(

View on GitHub (pinned to a2e22f3302)

Solutions

  1. Pass the id exactly as returned by the backup-listing API (SkillBackupEntry.backup_id) — do not build it from parts
  2. Trim the input before calling restore/delete
  3. If you maintain a caller, validate ids against the listing result set rather than trusting free-text input

Example fix

// before
let path = backup_path_for_id(&format!("{user_input}"))?;

// after — only accept ids that exist in the listing
let ids: HashSet<_> = list_backups()?.iter().map(|b| b.backup_id.clone()).collect();
if !ids.contains(user_input.trim()) { return Err(anyhow!("unknown backup id")); }
let path = backup_path_for_id(user_input.trim())?;
Defensive patterns

Strategy: type-guard

Validate before calling

// Rust — accept only ids present in the current backup listing
let valid: HashSet<&str> = backups.iter().map(|b| b.backup_id.as_str()).collect();
if !valid.contains(candidate.trim()) {
    return Err(anyhow!("unknown backup id"));
}

Type guard

// Rust predicate mirroring the guard
fn is_valid_backup_id(id: &str) -> bool {
    !id.trim().is_empty() && !id.contains("..") && !id.contains('/') && !id.contains('\\')
}

Try / catch

match backup_path_for_id(&id) {
    Err(e) if e.to_string().contains("Invalid backup id") => {
        // re-fetch the listing and pass an id from it verbatim; never sanitize-and-retry with a mutated id
    }
    other => other,
}

Prevention

When it happens

Trigger: Restore/delete-by-id called with a stale, garbled, or hand-constructed id: UI passing an id with surrounding whitespace, an id concatenated from user text, or a scripted/deeplink call supplying '../../something'.

Common situations: Frontend passing the wrong field (e.g. skill name or timestamp instead of backup_id); ids copied from logs with line breaks; automated tooling constructing ids from dates.

Related errors


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