{"record":{"id":"706d53bd75e6f42e","repo":"jdx/mise","slug":"brew-cask-binary-appdir-target-target-name-m","errorCode":null,"errorMessage":"brew-cask: binary $APPDIR target '{target_name}' must stay below Applications","messagePattern":"brew-cask: binary \\$APPDIR target '(.+?)' must stay below Applications","errorType":"validation","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask.rs","lineNumber":6140,"sourceCode":"    roots\n        .iter()\n        .map(|root| root.display().to_string())\n        .collect::<Vec<_>>()\n        .join(\" or \")\n}\n\nfn binary_target_path(target_name: &str, appdir: &Path) -> Result<PathBuf> {\n    if target_name.contains('\\0') {\n        bail!(\"brew-cask: binary target contains NUL\");\n    }\n    if let Some(relative) = target_name.strip_prefix(\"$APPDIR/\") {\n        let relative = Path::new(relative);\n        if relative.components().next().is_none()\n            || relative\n                .components()\n                .any(|component| !matches!(component, Component::Normal(_)))\n        {\n            bail!(\"brew-cask: binary $APPDIR target '{target_name}' must stay below Applications\");\n        }\n        if !allowed_appdir_roots()?.iter().any(|root| root == appdir) {\n            bail!(\"brew-cask: invalid appdir '{}'\", appdir.display());\n        }\n        return Ok(appdir.join(relative));\n    }\n    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 {","sourceCodeStart":6122,"sourceCodeEnd":6158,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/system/packages/brew/cask.rs#L6122-L6158","documentation":"binary_target_path supports the $APPDIR/ placeholder for binaries that must live inside the Applications tree. After stripping \"$APPDIR/\", the remainder must be non-empty and consist only of Component::Normal entries — no '..', no '.', no embedded absolute segments. This bail rejects placeholders that would escape the Applications subtree.","triggerScenarios":"A binary target of exactly \"$APPDIR/\" (empty remainder), or \"$APPDIR/../Foo.app\", or any $APPDIR-relative target whose component list contains a non-Normal component.","commonSituations":"Hand-written casks using $APPDIR incorrectly; malicious or malformed taps; copy-paste errors like \"$APPDIR//tool\" or \"$APPDIR/./tool\".","solutions":["Use a clean single relative remainder: \"$APPDIR/Foo.app/Contents/MacOS/tool\"","Avoid '..' and '.' segments after $APPDIR/","If the target comes from a tap, report/fix the artifact stanza upstream"],"exampleFix":"// before (cask binary target)\n\"target\": \"$APPDIR/../Foo.app/Contents/MacOS/tool\"\n// after\n\"target\": \"$APPDIR/Foo.app/Contents/MacOS/tool\"","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"use std::path::{Component, Path};\nfn is_valid_appdir_binary_target(name: &str) -> bool {\n    match name.strip_prefix(\"$APPDIR/\") {\n        Some(rest) => {\n            let rel = Path::new(rest);\n            rel.components().next().is_some()\n                && rel.components().all(|c| matches!(c, Component::Normal(_)))\n        }\n        None => true, // non-$APPDIR targets follow other rules\n    }\n}","tryCatchPattern":"match binary_target_path(name, &appdir) {\n    Ok(p) => p,\n    Err(e) if e.to_string().contains(\"must stay below Applications\") => {\n        warn!(\"skipping unsafe $APPDIR binary target: {name}\");\n        continue;\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Keep $APPDIR targets a single clean relative remainder with no '..', '.', or empty segments","Use $APPDIR only as a leading prefix followed by exactly one '/'"],"tags":["brew-cask","path-traversal","placeholder","binary"],"backgroundTag":"path-traversal-validation","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}