{"record":{"id":"995f7f5ffe1c86e0","repo":"jdx/mise","slug":"brew-cask-kind-name-must-stay-below-applica","errorCode":null,"errorMessage":"brew-cask: {kind} '{name}' must stay below Applications","messagePattern":"brew-cask: (.+?) '(.+?)' must stay below Applications","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask/mod.rs","lineNumber":3582,"sourceCode":"    let relative = source.strip_prefix(final_caskroom).ok()?;\n    if relative\n        .components()\n        .any(|component| matches!(component, Component::ParentDir))\n    {\n        return None;\n    }\n    Some(root.join(relative))\n}\n\n/// Rejects an empty relative path and any component that could climb out of\n/// `$APPDIR` (`..`, a root, a prefix).\nfn reject_appdir_escape(relative: &Path, kind: &str, name: &str) -> Result<()> {\n    if relative.components().next().is_none()\n        || relative\n            .components()\n            .any(|component| !matches!(component, Component::Normal(_)))\n    {\n        bail!(\"brew-cask: {kind} '{name}' must stay below Applications\");\n    }\n    Ok(())\n}\n\nfn resolve_symlink_target(link: &Path, target: PathBuf) -> PathBuf {\n    if target.is_absolute() {\n        target\n    } else {\n        link.parent()\n            .map(|parent| parent.join(&target))\n            .unwrap_or(target)\n    }\n}\n\nfn durable_internal_symlink_source(stage: &Path, source: &Path, link: &Path) -> Option<PathBuf> {\n    if !source.is_absolute()\n        || staged_relative_path(stage, source).is_none()\n        || staged_relative_path(stage, link).is_none()","sourceCodeStart":3564,"sourceCodeEnd":3600,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/packages/brew/cask/mod.rs#L3564-L3600","documentation":"reject_appdir_escape validates that a staged artifact's relative path stays strictly below the Applications directory: the path must be non-empty and every component must be a plain Normal component. Any '..', '.', root-prefix, or other special component (or an empty path) causes this bail, preventing cask artifacts from being written outside the intended appdir subtree.","triggerScenarios":"Staging a cask artifact (app/pkg/binary stanza) whose declared relative target path is empty or contains a non-Normal Path component such as ParentDir ('..'), CurDir ('.'), a RootDir/Prefix, per reject_appdir_escape(relative, kind, name).","commonSituations":"A cask stanza with a target like '../Utilities/Foo.app' or '/ absolute/path'; empty target strings produced by bad parsing; casks upgraded to a version with a malformed artifact path.","solutions":["Fix the cask artifact's relative path so it is non-empty and contains only plain path components (no '..', '.', or absolute prefixes)","If the artifact genuinely belongs elsewhere, adjust the cask's appdir/staging root instead of using '..' in the path","Verify you are using a trusted cask definition — this rejection often indicates a corrupt or malicious stanza","Re-parse the cask config to ensure empty strings were not produced by a parsing bug"],"exampleFix":"// before: escapes Applications\nlet relative = Path::new(\"../Utilities/MyApp.app\");\n// after: stays below Applications\nlet relative = Path::new(\"MyApp.app\");","handlingStrategy":"validation","validationCode":"use std::path::{Component, Path};\nfn stays_below_appdir(relative: &Path) -> bool {\n    !relative.as_os_str().is_empty()\n        && relative.components().all(|c| matches!(c, Component::Normal(_)))\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Express cask artifact targets as plain relative paths with no '..', '.', or leading '/'","Normalize user/registry-supplied paths before writing them into cask stanzas","Test cask stanza paths with a component check before shipping them"],"tags":["brew","cask","path-traversal","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}