{"record":{"id":"e948ac00b3dc8584","repo":"jdx/mise","slug":"brew-cask-binary-target-must-not-contain","errorCode":null,"errorMessage":"brew-cask: binary target '{}' must not contain '..'","messagePattern":"brew-cask: binary target '(.+?)' must not contain '\\.\\.'","errorType":"validation","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask.rs","lineNumber":6165,"sourceCode":"    if target_name.contains(\"$APPDIR\") {\n        bail!(\"brew-cask: $APPDIR must prefix a binary target\");\n    }\n    let prefix = prefix::prefix();\n    let prefix_str = prefix.to_string_lossy();\n    let target_name = target_name.replace(\"$HOMEBREW_PREFIX\", prefix_str.as_ref());\n    let path = PathBuf::from(&target_name);\n    let target = if path.is_absolute() {\n        path\n    } else if target_name.contains('/') {\n        prefix.join(path)\n    } else {\n        prefix.join(\"bin\").join(path)\n    };\n    if target\n        .components()\n        .any(|component| matches!(component, Component::ParentDir))\n    {\n        bail!(\n            \"brew-cask: binary target '{}' must not contain '..'\",\n            target.display()\n        );\n    }\n    let roots = allowed_binary_target_roots();\n    if !roots.iter().any(|root| target.starts_with(root)) {\n        bail!(\n            \"brew-cask: binary target '{}' must be under {}\",\n            target.display(),\n            allowed_binary_target_roots_display(&roots)\n        );\n    }\n    Ok(target)\n}\n\nfn installed_version(token: &str) -> Option<String> {\n    let versions = installed_versions(token);\n    match versions.as_slice() {","sourceCodeStart":6147,"sourceCodeEnd":6183,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/system/packages/brew/cask.rs#L6147-L6183","documentation":"binary_target_path expands $HOMEBREW_PREFIX, anchors relative targets under the brew prefix (or prefix/bin for bare names), then rejects any result containing a Component::ParentDir ('..'). This prevents a binary target from escaping the allowed install roots after expansion and anchoring.","triggerScenarios":"A binary target like \"../sbin/tool\", \"$HOMEBREW_PREFIX/../usr/bin/tool\", or any target that yields a '..' component after expansion/anchoring.","commonSituations":"Malformed tap casks; hand-edited cask overrides trying to redirect binaries outside the prefix; templated targets that concatenate '..' fragments.","solutions":["Use a clean target: bare \"tool\" (goes to <prefix>/bin), \"sbin/tool\" (goes to <prefix>/sbin), or \"$HOMEBREW_PREFIX/...\" without '..'","Report/fix the malformed artifact stanza in the tap","Verify with `brew info --json=v2 --cask <token>` what target is actually published"],"exampleFix":"// before (cask binary target)\n\"target\": \"../sbin/tool\"\n// after\n\"target\": \"sbin/tool\"","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"use std::path::{Component, Path};\nfn binary_target_has_no_dotdot(name: &str, homebrew_prefix: &str) -> bool {\n    let expanded = name.replace(\"$HOMEBREW_PREFIX\", homebrew_prefix);\n    !Path::new(&expanded)\n        .components()\n        .any(|c| matches!(c, Component::ParentDir))\n}","tryCatchPattern":"match binary_target_path(name, &appdir) {\n    Ok(p) => p,\n    Err(e) if e.to_string().contains(\"must not contain '..'\") => {\n        warn!(\"skipping binary target with '..': {name}\");\n        continue;\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Express binary targets as bare names or prefix-relative paths without '..'","Expand $HOMEBREW_PREFIX yourself when linting cask targets — traversal after expansion is what is checked"],"tags":["brew-cask","path-traversal","binary","path-validation"],"backgroundTag":"path-traversal-validation","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}