{"record":{"id":"6b8fef3ea2823426","repo":"jdx/mise","slug":"remote-cache-path-escapes-its-output-root","errorCode":null,"errorMessage":"remote cache path escapes its output root","messagePattern":"remote cache path escapes its output root","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/task/task_cache_store.rs","lineNumber":511,"sourceCode":"    async fn remove(&self, _key: &str) -> Result<()> {\n        // Ordinary cache writers intentionally have no remote-delete authority.\n        Ok(())\n    }\n\n    fn touch(&self, _key: &str) {}\n}\n\nfn validate_cache_path(path: &Path) -> Result<()> {\n    if path.as_os_str().is_empty() || path.is_absolute() {\n        bail!(\"remote cache path must be relative\");\n    }\n    if path.components().any(|component| {\n        matches!(\n            component,\n            Component::ParentDir | Component::RootDir | Component::Prefix(_)\n        )\n    }) {\n        bail!(\"remote cache path escapes its output root\");\n    }\n    Ok(())\n}\n\nfn cache_name(path: &Path) -> Result<String> {\n    let name = path\n        .file_name()\n        .and_then(|name| name.to_str())\n        .ok_or_else(|| eyre!(\"remote cache paths must be valid UTF-8\"))?;\n    if name.is_empty() || name == \".\" || name == \"..\" || name.contains(['/', '\\0']) {\n        bail!(\"invalid remote cache path component\");\n    }\n    Ok(name.to_string())\n}\n\nfn validate_cache_name(name: &str) -> Result<()> {\n    let path = Path::new(name);\n    if name.is_empty()","sourceCodeStart":493,"sourceCodeEnd":529,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/task/task_cache_store.rs#L493-L529","documentation":"validate_cache_path rejects paths containing '..', root, or Windows prefix components because such paths can escape the artifact output directory during materialization. The store treats this as a hostile or corrupt cache entry and refuses it.","triggerScenarios":"get(), archive_to_cas(), or materialize_remote_tree() encounter a cache path with a ParentDir ('..'), RootDir, or Prefix component (task_cache_store.rs:511), e.g. a tar entry like ../../etc/passwd or C:\\ in the manifest roots.","commonSituations":"Malicious or compromised remote cache server serving traversal paths; archives built with '../' members; manually edited manifests; cross-platform archives carrying Windows drive prefixes.","solutions":["Do not materialize the entry; purge that cache key/blob from the remote cache.","Re-run the task with --force to repopulate the entry from a trusted local execution.","Audit the remote cache endpoint/credentials — this pattern often indicates a compromised or misconfigured cache.","If the archive is self-produced, rebuild it so no member contains '..' or absolute components."],"exampleFix":"// before: archive contains '..' entries\n$ tar -czf out.tar.zst ../dist\n// after\n$ cd project && tar -czf out.tar.zst ./dist","handlingStrategy":"validation","validationCode":"use std::path::Component;\nfn contains_escape(p: &std::path::Path) -> bool {\n    p.components().any(|c| matches!(c, Component::ParentDir | Component::RootDir | Component::Prefix(_)))\n}","typeGuard":"fn is_contained(p: &std::path::Path) -> bool {\n    use std::path::Component::*;\n    p.components().all(|c| matches!(c, Normal(_) | CurDir))\n}","tryCatchPattern":"if let Err(e) = validate_cache_path(&p) {\n    if e.to_string().contains(\"escapes its output root\") {\n        // treat as hostile entry: skip and purge\n        return Ok(SkipReason::UnsafePath);\n    }\n    return Err(e);\n}","preventionTips":["Never archive trees containing '..' members; normalize paths before packaging.","Only consume cache entries from trusted cache servers over authenticated transports.","Purge, don't reuse, any entry flagged with traversal components.","Audit remote cache credentials — traversal entries often indicate compromise."],"tags":["cache","path-traversal","security","validation"],"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"}