{"record":{"id":"0a3d92b84579eade","repo":"jdx/mise","slug":"brew-cask-refusing-generic-artifact-copy-outside-0a3d92","errorCode":null,"errorMessage":"brew-cask: refusing generic artifact copy outside Homebrew prefix: {}","messagePattern":"brew-cask: refusing generic artifact copy outside Homebrew prefix: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask/mod.rs","lineNumber":1911,"sourceCode":"    )?;\n    if bound.st_dev != linked.st_dev || bound.st_ino != linked.st_ino {\n        bail!(\"brew-cask: temporary artifact directory was replaced\");\n    }\n    nix::unistd::unlinkat(\n        &parent.fd,\n        staging_name,\n        nix::unistd::UnlinkatFlags::RemoveDir,\n    )?;\n    Ok(())\n}\n\nfn validate_generic_copy_target(target: &Path) -> Result<()> {\n    let prefix = prefix::prefix();\n    if !target.starts_with(&prefix)\n        || target.strip_prefix(&prefix)?.components().next().is_none()\n        || !path_starts_with_resolved_root(target, &prefix)\n    {\n        bail!(\n            \"brew-cask: refusing generic artifact copy outside Homebrew prefix: {}\",\n            target.display()\n        );\n    }\n    Ok(())\n}\n\n#[cfg(unix)]\nstruct TrustedOperationParent {\n    fd: std::os::fd::OwnedFd,\n}\n\n#[cfg(unix)]\nimpl TrustedOperationParent {\n    fn path(&self) -> Result<PathBuf> {\n        #[cfg(target_os = \"linux\")]\n        return Ok(\n            Path::new(\"/proc/self/fd\").join(std::os::fd::AsRawFd::as_raw_fd(&self.fd).to_string())","sourceCodeStart":1893,"sourceCodeEnd":1929,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/packages/brew/cask/mod.rs#L1893-L1929","documentation":"`validate_generic_copy_target` enforces that every generic artifact copy destination lives inside the Homebrew prefix. It rejects targets that are not under `prefix::prefix()`, that resolve to the prefix itself (empty first component after strip_prefix), or whose resolved real path escapes the prefix via symlinks (`path_starts_with_resolved_root`). This is a path-escape guard so a malicious cask cannot write artifacts to arbitrary system locations via symlinked paths.","triggerScenarios":"A generic artifact copy whose `target` path (a) lies outside the Homebrew prefix, (b) equals the prefix itself with no remaining components, or (c) is a symlinked path that resolves outside the prefix — all reachable through any cask install/artifact-staging call that goes through `validate_generic_copy_target`.","commonSituations":"A cask artifact stanza with an absolute destination outside the brew prefix (e.g. `/usr/local/bin` on a non-Homebrew-managed layout); `HOMEBREW_PREFIX` pointing somewhere unexpected so valid destinations fall outside it; a symlinked prefix component (e.g. `/opt/homebrew` aliased through another path) making the resolved root differ from the lexical prefix.","solutions":["Check the printed target path against your actual prefix (`brew --prefix`) and correct the artifact destination to live inside it.","Verify the Homebrew prefix configuration (HOMEBREW_PREFIX / install location) matches where the artifacts are expected; a mismatched prefix causes legitimate targets to be rejected.","Remove intermediate symlinks between the target and the prefix, or reference the target via its real path under the prefix so the resolved-root check passes.","If the cask genuinely must install outside the prefix, use the appropriate cask stanza/mechanism for that (not the generic copier), which has its own validation."],"exampleFix":"// before: absolute destination outside the prefix\nlet target = Path::new(\"/usr/local/bin/mytool\");\n// after: derive destination from the prefix\nlet target = prefix::prefix().join(\"bin\").join(\"mytool\");","handlingStrategy":"validation","validationCode":"use std::path::{Path, PathBuf};\nfn target_inside_prefix(target: &Path, prefix: &Path) -> bool {\n    target.is_absolute()\n        && target.starts_with(prefix)\n        && target.strip_prefix(prefix).map(|r| r.components().next().is_some()).unwrap_or(false)\n        && target.canonicalize().ok().filter(|r| r.starts_with(prefix)).is_some()\n}\n","typeGuard":"fn is_safe_prefix_target(t: &Path, prefix: &Path) -> bool {\n    t.starts_with(prefix) && t.canonicalize().map(|c| c.starts_with(prefix)).unwrap_or(false)\n}\n","tryCatchPattern":null,"preventionTips":["Always derive artifact destinations from the brew prefix, never hardcode absolute paths.","Verify HOMEBREW_PREFIX matches the actual install location.","Avoid symlinked path components between the target and the prefix; use canonical paths."],"tags":["security","path-traversal","validation","filesystem"],"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"}