{"record":{"id":"08573704413e1db9","repo":"jdx/mise","slug":"invalid-recovery-destination","errorCode":null,"errorMessage":"invalid recovery destination","messagePattern":"invalid recovery destination","errorType":"validation","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/533346cc374382b41ec5ff70536252b2e96e725c/src/system/history/recovery.rs#L93-L129","documentation":"validate_destination guards the recovery path passed to recover_path. The library refuses any destination that is not an absolute path or whose components include a `..` (ParentDir) component, because recovery writes pre-snapshot content back to a fixed location and relative/parent-relative paths could redirect writes anywhere on disk. This is a deliberate security hardening against path traversal, not an incidental failure.","triggerScenarios":"Calling recover_path (directly or via a history recovery command) with a relative path like `foo/bar`, a path built with `..` such as `/a/../etc/x`, or a path containing `..` anywhere in its components even if it normalizes away.","commonSituations":"Passing a user-supplied or CLI-relative path without canonicalizing it first; constructing destinations with `Path::new(base).join(rel)` where rel came from config and contains `..`; scripts run from different working directories producing relative paths.","solutions":["Convert the destination to an absolute canonical path first (e.g. `std::path::absolute()` or `fs::canonicalize`) and strip any `..` components before calling recover_path","Reject or normalize the path at the source (config parse, CLI arg validation) so `..` never reaches the API","Ensure the caller passes a true absolute path (starting with `/`) not a path relative to cwd"],"exampleFix":"// before\nrecover_path(Path::new(\"../data/state\"), prior)?;\n// after\nlet dest = std::path::absolute(\"../data/state\")?; // still fails if it keeps `..`\nlet dest = dunce::canonicalize(dest)?;             // resolve/remove `..` components\nrecover_path(&dest, prior)?;","handlingStrategy":"validation","validationCode":"fn is_safe_dest(p: &std::path::Path) -> bool {\n    use std::path::Component;\n    p.is_absolute() && !p.components().any(|c| matches!(c, Component::ParentDir))\n}\nif !is_safe_dest(&dest) { /* reject before calling recover_path */ }","typeGuard":"fn safe_abs_path(p: &std::path::Path) -> Option<std::path::PathBuf> {\n    use std::path::Component;\n    let abs = std::path::absolute(p).ok()?;\n    match abs.components().any(|c| matches!(c, Component::ParentDir)) {\n        true => None,\n        false => Some(abs),\n    }\n}","tryCatchPattern":null,"preventionTips":["Always canonicalize/absolutize user-supplied paths before recovery calls","Validate CLI path arguments at the boundary and reject `..` early","Add a unit test asserting recover_path rejects relative and `..`-containing paths"],"tags":["path","validation","security"],"backgroundTag":"path-traversal-blocked","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}