{"record":{"id":"7245533563c3d2d7","repo":"jdx/mise","slug":"invalid-path-inside-recovery-directory","errorCode":null,"errorMessage":"invalid path inside recovery directory","messagePattern":"invalid path inside recovery directory","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/history/recovery.rs","lineNumber":169,"sourceCode":"            read_blob(state_dir, content)?;\n        }\n        PathSnapshot::Dir {\n            files, links, dirs, ..\n        } => {\n            let mut seen = BTreeSet::new();\n            for relative in files\n                .iter()\n                .map(|f| &f.rel)\n                .chain(links.iter().map(|f| &f.rel))\n                .chain(dirs.iter().map(|f| &f.rel))\n            {\n                if relative.as_os_str().is_empty()\n                    || !relative\n                        .components()\n                        .all(|c| matches!(c, Component::Normal(_)))\n                    || !seen.insert(relative)\n                {\n                    bail!(\"invalid path inside recovery directory\");\n                }\n            }\n            for leaf in files\n                .iter()\n                .map(|f| &f.rel)\n                .chain(links.iter().map(|f| &f.rel))\n            {\n                if seen\n                    .iter()\n                    .any(|other| *other != leaf && other.starts_with(leaf))\n                {\n                    bail!(\"recovery directory descends through a file or symlink\");\n                }\n            }\n            for file in files {\n                read_blob(state_dir, &file.content)?;\n            }\n        }","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/history/recovery.rs#L151-L187","documentation":"When validating a Dir snapshot, every recorded relative path must be non-empty, contain only Normal components (no `..`, `.`, root, or prefix), and be unique. This bail means the snapshot's internal path list is malformed — empty, traversal-like, or duplicated. It guards against restoring files outside the recovery directory from corrupt or crafted journal data.","triggerScenarios":"validate_snapshot -> PathSnapshot::Dir branch encounters a file/link/dir entry whose `rel` is empty, contains ParentDir/CurDir/root components, or duplicates an already-seen relative path.","commonSituations":"Hand-edited or corrupted pending-operation JSON, an injected/malicious journal, or a bug in snapshot capture producing duplicate or non-canonical relative paths.","solutions":["Inspect the pending-operation JSON for the offending `rel` entry and correct it to a clean relative path like `sub/file.txt`.","Discard the corrupt record and accept current contents; restore files manually from your dotfiles repository.","Report as a bug if the state directory was never edited — capture should never produce such paths."],"exampleFix":"// before (malicious/corrupt entry)\n\"rel\": \"../../.ssh/authorized_keys\"\n\n// after (clean relative path)\n\"rel\": \"git/config\"","handlingStrategy":"validation","validationCode":"fn rel_is_safe(rel: &std::path::Path) -> bool {\n    !rel.as_os_str().is_empty()\n        && rel.components().all(|c| matches!(c, std::path::Component::Normal(_)))\n}\n// check every rel entry in the snapshot before recovery","typeGuard":null,"tryCatchPattern":"match result { Err(e) if e.to_string().contains(\"invalid path inside recovery directory\") => /* snapshot corrupt or tampered; discard record and restore manually */, other => other? }","preventionTips":["Never edit pending-operation JSON by hand","Keep state directory writable only by the owning user","Treat 'invalid path' errors as possible tampering; audit the journal before re-running"],"tags":["path-validation","path-traversal","snapshot","security"],"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-14T00:17:10.932Z"}