{"record":{"id":"08573704413e1db9","repo":"jdx/mise","slug":"invalid-recovery-destination","errorCode":null,"errorMessage":"invalid recovery destination","messagePattern":"invalid recovery destination","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/history/recovery.rs","lineNumber":111,"sourceCode":"                after,\n                PathState::Dir {\n                    identity: Some(_),\n                    ..\n                }\n            ))\n    {\n        bail!(\"directory contents cannot be verified safely; left untouched\");\n    }\n    validate_snapshot(state_dir, prior)?;\n    if PathState::observe(path) != *after {\n        bail!(\"changed while preparing recovery; left untouched\");\n    }\n    restore(state_dir, path, prior)\n}\n\nfn validate_destination(path: &Path) -> Result<()> {\n    if !path.is_absolute() || path.components().any(|c| matches!(c, Component::ParentDir)) {\n        bail!(\"invalid recovery destination\");\n    }\n    for parent in path.ancestors().skip(1) {\n        if std::fs::symlink_metadata(parent).is_ok_and(|meta| meta.is_symlink()) {\n            bail!(\"a parent directory is now a symlink; left untouched\");\n        }\n    }\n    Ok(())\n}\n\nfn validate_blob_id(hash: &str) -> Result<()> {\n    if hash.len() != 64 || !hash.bytes().all(|b| b.is_ascii_hexdigit()) {\n        bail!(\"invalid recovery content identifier\");\n    }\n    Ok(())\n}\n\npub(super) fn read_blob(state_dir: &Path, blob: &Blob) -> Result<Vec<u8>> {\n    use base64::Engine;","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/history/recovery.rs#L93-L129","documentation":"validate_destination rejects a recovery destination that is not an absolute path or contains a `..` (ParentDir) component. This is a safety check so the restore step can never write outside the intended location or depend on relative-path resolution. It indicates corrupt or malicious journal data rather than anything the user did at runtime.","triggerScenarios":"recover_path -> validate_destination receives a journal entry whose PathChanged path is relative or contains `..` components; typically only possible with a hand-edited or corrupted pending-operation journal in the state directory.","commonSituations":"A corrupted state directory, a journal file modified by hand or by an untrusted tool, or a bug in code that constructed journal entries with non-canonicalized paths.","solutions":["Inspect the pending journal in the state directory for entries with relative or `..` paths and correct or remove them.","Delete the corrupt pending-operation record (accepting current file contents) if its journal cannot be trusted.","Report it as a bug if no one edited the state directory: journal entries should always carry canonical absolute paths."],"exampleFix":"// before (corrupt journal entry)\n\"path\": \"../home/user/.zshrc\"\n\n// after (canonical absolute path)\n\"path\": \"/home/user/.zshrc\"","handlingStrategy":"validation","validationCode":"// before trusting a journal, verify destinations\nfn is_safe_destination(p: &std::path::Path) -> bool {\n    p.is_absolute() && !p.components().any(|c| matches!(c, std::path::Component::ParentDir))\n}","typeGuard":null,"tryCatchPattern":"match result { Err(e) if e.to_string().contains(\"invalid recovery destination\") => /* journal corrupt; discard record and restore manually */, other => other? }","preventionTips":["Never hand-edit pending-operation journal files","Restrict write access to the mise state directory (permissions 0700)","Canonicalize paths to absolute form before recording operations"],"tags":["path-validation","security","path-traversal","recovery"],"backgroundTag":"path-traversal-blocked","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}