{"record":{"id":"c0e55cf90ff74871","repo":"jdx/mise","slug":"brew-cask-app-target-target-name-must-not-con","errorCode":null,"errorMessage":"brew-cask: app target '{target_name}' must not contain '..'","messagePattern":"brew-cask: app target '(.+?)' must not contain '\\.\\.'","errorType":"validation","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask.rs","lineNumber":5989,"sourceCode":"            return false;\n        }\n    }\n    true\n}\n\nfn app_target_path(target_name: &str) -> Result<PathBuf> {\n    let app_dir = target_app_dir()?;\n    if target_name.contains('\\0') {\n        bail!(\"brew-cask: app target contains NUL\");\n    }\n    if target_name.contains('/') {\n        let target = target_name.replace(\"$HOMEBREW_PREFIX\", &prefix::prefix().to_string_lossy());\n        let path = PathBuf::from(target);\n        if path\n            .components()\n            .any(|component| matches!(component, Component::ParentDir))\n        {\n            bail!(\"brew-cask: app target '{target_name}' must not contain '..'\");\n        }\n        if path.is_absolute() {\n            let prefix_app_dir = prefix::prefix().join(\"Applications\");\n            if path.starts_with(&app_dir) || path.starts_with(&prefix_app_dir) {\n                return Ok(path);\n            }\n            // Casks routinely hardcode an absolute `/Applications/Foo.app`\n            // target. When an override appdir is configured, relocate such a\n            // target into it (preserving any subdirectories) rather than\n            // rejecting it. `$HOMEBREW_PREFIX`-anchored targets are handled by\n            // the check above and are never relocated.\n            if app_dir != Path::new(DEFAULT_APP_DIR)\n                && let Ok(rest) = path.strip_prefix(DEFAULT_APP_DIR)\n            {\n                return Ok(app_dir.join(rest));\n            }\n            bail!(\n                \"brew-cask: app target '{target_name}' must be under {}\",","sourceCodeStart":5971,"sourceCodeEnd":6007,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/system/packages/brew/cask.rs#L5971-L6007","documentation":"When a cask 'app' target contains '/', mise expands $HOMEBREW_PREFIX, parses it into path components, and rejects any Component::ParentDir ('..'). This keeps artifact linking confined so a cask cannot write a bundle outside its declared destination — a standard path-traversal containment check before symlinks are created, potentially with elevated privileges.","triggerScenarios":"An app artifact target such as \"../Foo.app\", \"/Applications/../../Library/Foo.app\", or any target whose component list contains '..' reaching app_target_path.","commonSituations":"Hand-edited cask override files; a malicious tap attempting to escape /Applications; a mis-typed target in a custom local cask.","solutions":["Fix the cask's target to a clean path: a bare bundle name (\"Foo.app\"), or an absolute path without '..' segments","If the target comes from a tap, report the malformed artifact stanza upstream or remove the tap","Verify with `brew info --json=v2 --cask <token>` what target Homebrew actually sees","Update mise before assuming the rejection is a bug"],"exampleFix":"// before (cask artifact target)\n\"target\": \"/Applications/../../Library/Foo.app\"\n// after\n\"target\": \"Foo.app\"","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"use std::path::{Component, Path};\nfn app_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 app_target_path(name) {\n    Ok(p) => p,\n    Err(e) if e.to_string().contains(\"must not contain '..'\") => {\n        warn!(\"skipping cask app target with '..': {name}\");\n        continue;\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Write cask targets as bare bundle names unless an absolute path is required","Lint custom casks for '..' segments before installing","Remember $HOMEBREW_PREFIX is expanded first — a '..' after the prefix counts too"],"tags":["brew-cask","path-traversal","path-validation","hardening"],"backgroundTag":"path-traversal-validation","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}