{"record":{"id":"76c67b62f58702ae","repo":"jdx/mise","slug":"brew-cask-staged-symlink-path-escaped-extraction","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":"critical","filePath":"src/system/packages/brew/cask.rs","lineNumber":1572,"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":1554,"sourceCodeEnd":1590,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/system/packages/brew/cask.rs#L1554-L1590","documentation":"When mise promotes the staged extraction into its owned stage directory, every staged path must map back inside the extraction root. If a staged file's parent no longer sits under the resolved stage root — the signature of symlinks in the archive resolving through absolute targets or ../-escapes — the copy is refused: the archive is trying to write outside its sandbox.","triggerScenarios":"An extracted archive contains symlinks whose chain resolves outside the stage directory (e.g. link -> ../../../../usr/local/lib), so resolving the staged tree escapes the extraction root during promotion to the owned stage.","commonSituations":"Supply-chain probing of mise's cask pipeline; tarballs packed with absolute symlink targets; pathological packaging from niche taps.","solutions":["Do not install the cask; download its URL manually, list the archive contents, and report it — this guard blocks archive (zip-slip) escapes","If you package the artifact, rebuild it so every symlink is relative and stays inside the extraction root","Prefer casks served from the official Homebrew API where artifacts are widely exercised"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// If you build/audit cask archives before publishing, verify symlinks stay inside:\nfn symlinks_stay_in_root(root: &Path) -> Result<bool> {\n    for entry in walkdir::WalkDir::new(root).follow_links(false) {\n        let e = entry?;\n        if e.path_is_symlink() {\n            let t = std::fs::read_link(e.path())?;\n            if t.is_absolute() || t.components().any(|c| c == std::path::Component::ParentDir) {\n                return Ok(false);\n            }\n        }\n    }\n    Ok(true)\n}","typeGuard":null,"tryCatchPattern":"Catch the 'escaped extraction root' bails as a hard stop: quarantine the downloaded archive, log the cask token and URL, and report the cask — never retry or attempt manual extraction of the same payload.","preventionTips":["Treat any symlink-escape hit as a supply-chain red flag, not an inconvenience","Package archives with relative, in-root symlinks only","Use official Homebrew API casks where artifacts are broadly validated"],"tags":["homebrew","cask","symlink","zip-slip","security","mise"],"backgroundTag":"zip-slip","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}