{"record":{"id":"84cd949d47f5354e","repo":"jdx/mise","slug":"remote-cache-symlink-target-is-unsafe","errorCode":null,"errorMessage":"remote cache symlink target is unsafe","messagePattern":"remote cache symlink target is unsafe","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/task/task_cache_store.rs","lineNumber":552,"sourceCode":"        || !matches!(path.components().next(), Some(Component::Normal(_)))\n    {\n        bail!(\"invalid remote cache path component\");\n    }\n    Ok(())\n}\n\nfn validate_cache_symlink_target(path: &Path, target: &Path) -> Result<()> {\n    if target.is_absolute() {\n        bail!(\"remote cache symlink target must be relative\");\n    }\n    let resolved = path.parent().unwrap_or(Path::new(\"\")).join(target);\n    let mut depth = 0_i64;\n    for component in resolved.components() {\n        match component {\n            Component::Normal(_) => depth += 1,\n            Component::ParentDir => depth -= 1,\n            Component::CurDir => {}\n            _ => bail!(\"remote cache symlink target is unsafe\"),\n        }\n        if depth < 0 {\n            bail!(\"remote cache symlink target escapes its output root\");\n        }\n    }\n    Ok(())\n}\n\nfn archive_to_cas(path: &Path, staging_dir: &Path) -> Result<(CacheDigest, Vec<BlobUpload>)> {\n    file::create_dir_all(staging_dir)?;\n    let decoder = zstd::Decoder::new(File::open(path)?)?;\n    let mut archive = Archive::new(decoder);\n    let mut nodes = BTreeMap::<PathBuf, ArchiveNode>::new();\n    nodes.insert(PathBuf::new(), ArchiveNode::Directory { mode: 0o755 });\n\n    for entry in archive.entries()? {\n        let mut entry = entry?;\n        let entry_path = entry.path()?.into_owned();","sourceCodeStart":534,"sourceCodeEnd":570,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/task/task_cache_store.rs#L534-L570","documentation":"After the relative check, validate_cache_symlink_target resolves the target against the link's parent directory and walks its components. Only Normal, ParentDir, and CurDir components are allowed; anything else - in practice a Windows drive Prefix such as \"C:libs\" or a stray RootDir - bails with 'remote cache symlink target is unsafe'.","triggerScenarios":"A symlink target like \"C:libs\\x\" (a drive-relative Windows path: not absolute, so it passes the first check, but it parses with a Prefix component) stored in a cached archive or in a remote directory proto's symlinks list.","commonSituations":"Windows build outputs with drive-relative symlink targets; caches produced by tooling that stores Windows targets verbatim; cross-OS sharing of one remote cache between Windows and POSIX machines.","solutions":["Rewrite the symlink target as a plain relative path without a drive prefix","Avoid caching outputs that contain drive-relative links","Purge the cached entry and let the task rebuild the outputs cleanly"],"exampleFix":"# before: target carries a drive prefix (C:libs)\nlink -> C:libs\\tool\n# after: plain relative target\nlink -> libs/tool","handlingStrategy":"validation","validationCode":"use std::path::{Component, Path};\n\nfn symlink_target_is_safe(target: &Path) -> bool {\n    !target.is_absolute()\n        && target.components().all(|c| matches!(\n            c,\n            Component::Normal(_) | Component::ParentDir | Component::CurDir\n        ))\n}\n\nassert!(symlink_target_is_safe(&std::fs::read_link(link)?));","typeGuard":"fn symlink_target_is_safe(target: &std::path::Path) -> bool {\n    !target.is_absolute()\n        && target.components().all(|c| matches!(\n            c,\n            std::path::Component::Normal(_)\n                | std::path::Component::ParentDir\n                | std::path::Component::CurDir\n        ))\n}","tryCatchPattern":"match restore_or_commit(&task).await {\n    Ok(v) => v,\n    Err(err) if err.to_string().contains(\"symlink target is unsafe\") => {\n        purge_cache_entry(&task).await?; // rebuild without the drive-prefixed link\n        run_uncached(&task).await?\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["Never put drive-prefixed relative paths (C:foo) in symlink targets","Test cache restore on the same OS family that produced the cache","Purge shared caches when changing how task outputs build symlinks"],"tags":["mise","task-cache","symlink","windows","path-traversal"],"backgroundTag":"symlink-path-traversal","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}