{"record":{"id":"4694e21433881d76","repo":"farion1231/cc-switch","slug":"invalid-backup-id-backup-id","errorCode":null,"errorMessage":"Invalid backup id: {backup_id}","messagePattern":"Invalid backup id: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src-tauri/src/services/skill.rs","lineNumber":3465,"sourceCode":"        }\n\n        entries.sort_by_key(|(_, modified)| *modified);\n        let remove_count = entries.len().saturating_sub(SKILL_BACKUP_RETAIN_COUNT);\n\n        for (path, _) in entries.into_iter().take(remove_count) {\n            fs::remove_dir_all(&path)?;\n        }\n\n        Ok(())\n    }\n\n    fn backup_path_for_id(backup_id: &str) -> Result<PathBuf> {\n        if backup_id.contains(\"..\")\n            || backup_id.contains('/')\n            || backup_id.contains('\\\\')\n            || backup_id.trim().is_empty()\n        {\n            return Err(anyhow!(\"Invalid backup id: {backup_id}\"));\n        }\n\n        Ok(Self::get_backup_dir()?.join(backup_id))\n    }\n\n    fn read_backup_metadata(backup_path: &Path) -> Result<SkillBackupMetadata> {\n        let metadata_path = backup_path.join(\"meta.json\");\n        let content = fs::read_to_string(&metadata_path)\n            .with_context(|| format!(\"failed to read {}\", metadata_path.display()))?;\n        serde_json::from_str(&content)\n            .with_context(|| format!(\"failed to parse {}\", metadata_path.display()))\n    }\n\n    fn create_uninstall_backup(skill: &InstalledSkill) -> Result<Option<PathBuf>> {\n        Self::create_uninstall_backup_excluding(skill, None)\n    }\n\n    fn create_uninstall_backup_excluding(","sourceCodeStart":3447,"sourceCodeEnd":3483,"githubUrl":"https://github.com/farion1231/cc-switch/blob/a2e22f330273a5b6ffa87cb8b82b624601bac562/src-tauri/src/services/skill.rs#L3447-L3483","documentation":"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.","triggerScenarios":"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'.","commonSituations":"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.","solutions":["Pass the id exactly as returned by the backup-listing API (SkillBackupEntry.backup_id) — do not build it from parts","Trim the input before calling restore/delete","If you maintain a caller, validate ids against the listing result set rather than trusting free-text input"],"exampleFix":"// before\nlet path = backup_path_for_id(&format!(\"{user_input}\"))?;\n\n// after — only accept ids that exist in the listing\nlet ids: HashSet<_> = list_backups()?.iter().map(|b| b.backup_id.clone()).collect();\nif !ids.contains(user_input.trim()) { return Err(anyhow!(\"unknown backup id\")); }\nlet path = backup_path_for_id(user_input.trim())?;","handlingStrategy":"type-guard","validationCode":"// Rust — accept only ids present in the current backup listing\nlet valid: HashSet<&str> = backups.iter().map(|b| b.backup_id.as_str()).collect();\nif !valid.contains(candidate.trim()) {\n    return Err(anyhow!(\"unknown backup id\"));\n}","typeGuard":"// Rust predicate mirroring the guard\nfn is_valid_backup_id(id: &str) -> bool {\n    !id.trim().is_empty() && !id.contains(\"..\") && !id.contains('/') && !id.contains('\\\\')\n}","tryCatchPattern":"match backup_path_for_id(&id) {\n    Err(e) if e.to_string().contains(\"Invalid backup id\") => {\n        // re-fetch the listing and pass an id from it verbatim; never sanitize-and-retry with a mutated id\n    }\n    other => other,\n}","preventionTips":["Always source backup ids from the list-backups API response, never construct them from dates or names","Trim whitespace before calling restore/delete","Treat crafted ids from external input as path-traversal attempts and reject loudly"],"tags":["security","path-traversal","backup","validation"],"backgroundTag":null,"analyzedSha":"a2e22f330273a5b6ffa87cb8b82b624601bac562","analyzedAt":"2026-08-16T03:46:07.889Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}