{"record":{"id":"71bfd941be12ee6a","repo":"jdx/mise","slug":"relative-remote-mise-path-escapes-the-staged-proje","errorCode":null,"errorMessage":"relative remote mise path escapes the staged project: {command:?}","messagePattern":"relative remote mise path escapes the staged project: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/remote.rs","lineNumber":1252,"sourceCode":") -> Result<String> {\n    if command.starts_with('/') {\n        return Ok(command.to_string());\n    }\n    if let Some(suffix) = command.strip_prefix(\"~/\") {\n        if suffix.is_empty() {\n            bail!(\"remote mise path does not name an executable: {command:?}\");\n        }\n        let home = remote_home.ok_or_else(|| eyre!(\"remote login home was not resolved\"))?;\n        return Ok(format!(\"{}/{suffix}\", home.trim_end_matches('/')));\n    }\n\n    let mut components = Vec::new();\n    for component in command.split('/') {\n        match component {\n            \"\" | \".\" => {}\n            \"..\" => {\n                if components.pop().is_none() {\n                    bail!(\"relative remote mise path escapes the staged project: {command:?}\");\n                }\n            }\n            component => components.push(component),\n        }\n    }\n    if components.is_empty() {\n        bail!(\"relative remote mise path does not name an executable: {command:?}\");\n    }\n    Ok(format!(\"{project}/{}\", components.join(\"/\")))\n}\n\nfn remote_mise_find_script() -> &'static str {\n    r#\"mise_path=$(command -v mise 2>/dev/null || true)\ncase \"$mise_path\" in /*) ;; *) mise_path= ;; esac\nif [ -z \"$mise_path\" ]; then\n  for candidate in \"$HOME/.local/bin/mise\" \"$HOME/.local/share/mise/bin/mise\" \"$HOME/.cargo/bin/mise\" /usr/local/bin/mise /opt/homebrew/bin/mise; do\n    if [ -x \"$candidate\" ]; then\n      mise_path=$candidate","sourceCodeStart":1234,"sourceCodeEnd":1270,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/remote.rs#L1234-L1270","documentation":"For relative remote mise paths, mise normalizes components and rejects any path that would climb above the staged project root: when a \"..\" component has no matching parent component left, the path escapes the project and mise bails instead of executing outside it.","triggerScenarios":"Supplying a relative remote mise command like \"../tools/mise\" or \"a/../../mise\" whose normalization would resolve outside the staged project directory.","commonSituations":"Users assuming relative paths are relative to CWD or home rather than the staged project; paths copied from layouts where mise sat in a sibling directory.","solutions":["Use a path that stays within the staged project, e.g. \"bin/mise\" or \"./mise\"","Use an absolute path (starting with /) or home-relative \"~/...\" if the binary lives elsewhere","Install mise at the default location so no relative path is needed"],"exampleFix":"// before\nremote_mise = \"../tools/mise\"\n// after\nremote_mise = \"~/tools/mise\"","handlingStrategy":"validation","validationCode":"function staysInProject(rel) {\n  let depth = 0;\n  for (const seg of rel.split('/')) {\n    if (seg === '..') { depth--; if (depth < 0) return false; }\n    else if (seg && seg !== '.') depth++;\n  }\n  return true;\n}","typeGuard":"const inProject = (rel) => !path.posix.isAbsolute(rel) && !rel.split('/').reduce((d,s)=> s==='..'? d-1 : (s&&s!=='.')? d+1 : d, 0) < 0 === false;","tryCatchPattern":null,"preventionTips":["Keep relative remote paths rooted at the staged project","Use ~/ or absolute paths for binaries outside the project","Lint remote path settings for '..' segments"],"tags":["path","validation","remote","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-14T05:17:10.506Z"}