{"record":{"id":"0b5627bddb9c6065","repo":"jdx/mise","slug":"brew-cask-staged-symlink-path-escaped-extraction-0b5627","errorCode":null,"errorMessage":"brew-cask: staged symlink path escaped extraction root: {}","messagePattern":"brew-cask: staged symlink path escaped extraction root: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask/mod.rs","lineNumber":1435,"sourceCode":"}\n\nfn copy_staged_artifact_closure(stage: &Path, owned_stage: &Path, source: &Path) -> Result<()> {\n    let stage = lexically_normalized_path(stage);\n    let mut pending = vec![lexically_normalized_path(source)];\n    let mut visited = BTreeSet::new();\n    while let Some(source) = pending.pop() {\n        let relative = staged_relative_path(&stage, &source).ok_or_else(|| {\n            eyre!(\n                \"brew-cask: staged symlink target escaped extraction root: {}\",\n                source.display()\n            )\n        })?;\n        if relative.components().next().is_some()\n            && !source\n                .parent()\n                .is_some_and(|parent| path_starts_with_resolved_root(parent, &stage))\n        {\n            bail!(\n                \"brew-cask: staged symlink path escaped extraction root: {}\",\n                source.display()\n            );\n        }\n        if !visited.insert(relative.to_path_buf()) {\n            continue;\n        }\n        let destination = owned_stage.join(&relative);\n        let metadata = source.symlink_metadata()?;\n        if destination.symlink_metadata().is_err() {\n            if let Some(parent) = destination.parent() {\n                file::create_dir_all(parent)?;\n            }\n            if metadata.file_type().is_symlink() {\n                file::make_symlink(&std::fs::read_link(&source)?, &destination)?;\n            } else {\n                copy_cask_artifact(&source, &destination)?;\n            }","sourceCodeStart":1417,"sourceCodeEnd":1453,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/packages/brew/cask/mod.rs#L1417-L1453","documentation":"During installation of a brew cask on Linux/macOS, mise copies staged symlinked artifacts from the extraction (stage) directory into an owned staging area. Before copying each path it verifies the path's parent directory is still resolved-contained within the stage root. If a symlink chain or odd path component resolves to a location whose parent escapes the extraction root, the copy is aborted to prevent a malicious cask from planting files (or writing through symlinks) outside its own staged tree.","triggerScenarios":"A cask payload contains a symlink whose target — after resolve_symlink_target and lexical normalization — lands outside the stage directory, or the parent of the staged source path resolves (via symlinked ancestors) to a directory not under the stage. Triggered in copy_staged_artifact_closure while expanding the transitive symlink closure during durabilize_staged_symlink_targets.","commonSituations":"Installing a cask whose archive contains absolute symlinks pointing outside the payload (e.g. /usr/local/...), a crafted/compromised cask attempting symlink-based path traversal, or a corrupted/partially-extracted stage where an intermediate directory is itself a symlink out of the tree.","solutions":["Inspect the cask's payload (tar -tf or find -type l on the extracted stage) and remove/fix symlinks whose targets escape the extraction root","Re-download the cask — the archive may be corrupted or tampered with; verify checksums","Check whether the stage directory itself sits under a symlinked path; extract to a plain (non-symlinked) location","Report the cask upstream if it legitimately needs out-of-tree links; mise will not stage such artifacts by design"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// before staging, ensure every symlink under the stage resolves inside it\nfor link in walk_symlinks(stage) {\n    let target = resolve_symlink_target(&link, std::fs::read_link(&link)?);\n    if !target.starts_with(&stage) {\n        return Err(format!(\"symlink escapes stage: {} -> {}\", link.display(), target.display()));\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Extract cask payloads to a plain directory with no symlinked ancestors","Inspect archives for absolute or out-of-tree symlinks before trusting them","Re-download casks from trusted sources with checksum verification","Never manually edit or re-link files inside mise's staged cask directories"],"tags":["security","symlink","path-traversal","brew-cask"],"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"}