{"record":{"id":"87fd0ba3bb6364bd","repo":"jdx/mise","slug":"unsafe-source-repository-path","errorCode":null,"errorMessage":"unsafe source repository path","messagePattern":"unsafe source repository path","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/remote_repository.rs","lineNumber":233,"sourceCode":"        .args([\"clone\", \"--no-checkout\", \"--\"])\n        .arg(bundle)\n        .arg(&checkout)\n        .output()?;\n    if !output.status.success() {\n        bail!(\"invalid transferred repository bundle\");\n    }\n    if git(&checkout, &[\"rev-parse\", \"HEAD\"])? != revision {\n        bail!(\"transferred revision mismatch\");\n    }\n    let entries = git(&checkout, &[\"ls-tree\", \"-r\", \"-z\", \"--name-only\", revision])?;\n    for entry in entries.split('\\0').filter(|s| !s.is_empty()) {\n        let path = Path::new(entry);\n        if path\n            .components()\n            .any(|c| !matches!(c, std::path::Component::Normal(_)))\n            || entry.split('/').any(|p| p.eq_ignore_ascii_case(\".git\"))\n        {\n            bail!(\"unsafe source repository path\");\n        }\n        if entry.to_ascii_lowercase().ends_with(\".local.toml\") {\n            bail!(\n                \"source contains machine-local configuration ({entry}); remove it from the repository before onboarding\"\n            );\n        }\n    }\n    git(&checkout, &[\"remote\", \"set-url\", \"origin\", origin])?;\n    let branch = git(&checkout, &[\"symbolic-ref\", \"--short\", \"HEAD\"])?;\n    git(\n        &checkout,\n        &[\"-c\", \"core.hooksPath=/dev/null\", \"checkout\", &branch],\n    )?;\n    if destination.join(\".git\").exists() {\n        if git(destination, &[\"remote\", \"get-url\", \"origin\"])? != origin {\n            bail!(\"global configuration origin does not match\");\n        }\n        if !git(","sourceCodeStart":215,"sourceCodeEnd":251,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/remote_repository.rs#L215-L251","documentation":"While enumerating the repository tree with git ls-tree, install_at rejects any entry whose path contains non-Normal components (.., ., leading /) or a segment named .git (case-insensitive). This blocks path-traversal and .git-spoofing attacks when copying source files into the global config directory.","triggerScenarios":"A (potentially malicious) repository contains files like ../escape.toml, /abs/path.toml, .Git/config, or any path whose components aren't plain names.","commonSituations":"Onboarding an untrusted third-party config repository that includes dotfile symlinks or a nested .git directory; accidentally committing a submodule's .git entry.","solutions":["Remove unsafe entries (../, absolute paths, .git segments) from the source repository and commit the fix","Use a trusted, reviewed repository as the onboarding source","Pin a known-good revision of the cleaned repository"],"exampleFix":"# before (repo contains .Git/config and ../x.toml)\n# after (cleaned tree)\n$ git rm -r .Git ../x.toml && git commit -m \"clean unsafe paths\"","handlingStrategy":"validation","validationCode":"fn tree_paths_safe(entries: &[String]) -> bool {\n    entries.iter().all(|e| {\n        std::path::Path::new(e).components()\n            .all(|c| matches!(c, std::path::Component::Normal(_)))\n            && !e.split('/').any(|p| p.eq_ignore_ascii_case(\".git\"))\n    })\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only onboard repositories you have reviewed","Keep .git, submodule internals, and absolute/../ paths out of the config repo","Audit `git ls-tree -r --name-only <rev>` before onboarding an untrusted repo"],"tags":["security","path-traversal","git"],"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"}