jdx/mise · error

invalid recovery destination

Error message

invalid recovery destination

What it means

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.

Solutions

  1. 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
  2. Reject or normalize the path at the source (config parse, CLI arg validation) so `..` never reaches the API
  3. Ensure the caller passes a true absolute path (starting with `/`) not a path relative to cwd

Example fix

// before
recover_path(Path::new("../data/state"), prior)?;
// after
let dest = std::path::absolute("../data/state")?; // still fails if it keeps `..`
let dest = dunce::canonicalize(dest)?;             // resolve/remove `..` components
recover_path(&dest, prior)?;
Defensive patterns

Strategy: validation

Validate before calling

fn is_safe_dest(p: &std::path::Path) -> bool {
    use std::path::Component;
    p.is_absolute() && !p.components().any(|c| matches!(c, Component::ParentDir))
}
if !is_safe_dest(&dest) { /* reject before calling recover_path */ }

Type guard

fn safe_abs_path(p: &std::path::Path) -> Option<std::path::PathBuf> {
    use std::path::Component;
    let abs = std::path::absolute(p).ok()?;
    match abs.components().any(|c| matches!(c, Component::ParentDir)) {
        true => None,
        false => Some(abs),
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17). Data as JSON: /api/errors/08573704413e1db9. Report an issue: GitHub.

Appendix: source

Thrown at src/system/history/recovery.rs:111

                after,
                PathState::Dir {
                    identity: Some(_),
                    ..
                }
            ))
    {
        bail!("directory contents cannot be verified safely; left untouched");
    }
    validate_snapshot(state_dir, prior)?;
    if PathState::observe(path) != *after {
        bail!("changed while preparing recovery; left untouched");
    }
    restore(state_dir, path, prior)
}

fn validate_destination(path: &Path) -> Result<()> {
    if !path.is_absolute() || path.components().any(|c| matches!(c, Component::ParentDir)) {
        bail!("invalid recovery destination");
    }
    for parent in path.ancestors().skip(1) {
        if std::fs::symlink_metadata(parent).is_ok_and(|meta| meta.is_symlink()) {
            bail!("a parent directory is now a symlink; left untouched");
        }
    }
    Ok(())
}

fn validate_blob_id(hash: &str) -> Result<()> {
    if hash.len() != 64 || !hash.bytes().all(|b| b.is_ascii_hexdigit()) {
        bail!("invalid recovery content identifier");
    }
    Ok(())
}

pub(super) fn read_blob(state_dir: &Path, blob: &Blob) -> Result<Vec<u8>> {
    use base64::Engine;

View on GitHub (pinned to 533346cc37)