{"record":{"id":"ec6713fb5c55a3e3","repo":"jdx/mise","slug":"brew-cask-app-dir-env-must-not-contain","errorCode":null,"errorMessage":"brew-cask: {APP_DIR_ENV} '{}' must not contain '..'","messagePattern":"brew-cask: (.+?) '(.+?)' must not contain '\\.\\.'","errorType":"validation","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask.rs","lineNumber":6042,"sourceCode":"fn target_app_dir() -> Result<PathBuf> {\n    let Ok(dir) = crate::env::var(APP_DIR_ENV) else {\n        return Ok(PathBuf::from(DEFAULT_APP_DIR));\n    };\n    if dir.is_empty() {\n        return Ok(PathBuf::from(DEFAULT_APP_DIR));\n    }\n    let dir = PathBuf::from(dir);\n    if !dir.is_absolute() {\n        bail!(\n            \"brew-cask: {APP_DIR_ENV} '{}' must be an absolute path\",\n            dir.display()\n        );\n    }\n    if dir\n        .components()\n        .any(|component| matches!(component, Component::ParentDir))\n    {\n        bail!(\n            \"brew-cask: {APP_DIR_ENV} '{}' must not contain '..'\",\n            dir.display()\n        );\n    }\n    // Resolve the override to a real absolute path: canonicalize its longest\n    // existing prefix and re-append the components that do not exist yet. This\n    // makes the appdir a symlink-free containment boundary — privileged cask\n    // mutations then operate on resolved paths and cannot be redirected through\n    // a symlinked component — and it collapses every spelling of the filesystem\n    // root (`/`, `//`, `/.`, a symlink to `/`, ...) to `/` so they can all be\n    // rejected together.\n    let resolved = resolve_appdir(&dir);\n    if !resolved\n        .components()\n        .any(|component| matches!(component, Component::Normal(_)))\n    {\n        bail!(\n            \"brew-cask: {APP_DIR_ENV} '{}' must not resolve to the filesystem root\",","sourceCodeStart":6024,"sourceCodeEnd":6060,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/system/packages/brew/cask.rs#L6024-L6060","documentation":"After confirming MISE_BREW_CASK_OPT_APPDIR is absolute, target_app_dir rejects any value whose components contain '..'. Because the app dir is used as a symlink-free containment boundary for privileged writes, lexical parent-directory components would let a cask escape it. This mirrors the '..' checks applied to individual cask targets.","triggerScenarios":"Exporting MISE_BREW_CASK_OPT_APPDIR=\"/Applications/../opt/MyApps\" or any absolute path containing a '..' component.","commonSituations":"Users shortening paths with '..' in dotfiles or CI env; scripts concatenating path fragments that produce '..' segments.","solutions":["Export the normalized path without '..': `export MISE_BREW_CASK_OPT_APPDIR=/opt/MyApps`","Generate the value with realpath: `export MISE_BREW_CASK_OPT_APPDIR=\"$(realpath ~/Applications)\"`","Or unset the variable to fall back to /Applications"],"exampleFix":"# before\nexport MISE_BREW_CASK_OPT_APPDIR=/Applications/../opt/MyApps\n# after\nexport MISE_BREW_CASK_OPT_APPDIR=/opt/MyApps","handlingStrategy":"validation","validationCode":"use std::path::{Component, Path};\nfn appdir_env_components_ok(val: &str) -> bool {\n    let p = Path::new(val);\n    p.is_absolute() && !p.components().any(|c| matches!(c, Component::ParentDir))\n}","typeGuard":null,"tryCatchPattern":"match target_app_dir() {\n    Ok(dir) => dir,\n    Err(e) if e.to_string().contains(\"must not contain '..'\") => {\n        let clean = std::path::PathBuf::from(val).canonicalize()?;\n        std::env::set_var(\"MISE_BREW_CASK_OPT_APPDIR\", clean);\n        target_app_dir()\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Normalize the appdir with realpath before exporting it","Never build the value by string concatenation of fragments that may include '..'"],"tags":["brew-cask","env-var","path-traversal","appdir"],"backgroundTag":"invalid-env-var-value","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}