{"record":{"id":"50acbc9e564c3b07","repo":"jdx/mise","slug":"brew-cask-structured-flight-glob-matched-out","errorCode":null,"errorMessage":"brew-cask: structured flight glob '{}' matched outside staged path","messagePattern":"brew-cask: structured flight glob '(.+?)' matched outside staged path","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask.rs","lineNumber":3943,"sourceCode":"    let escaped_root = glob::Pattern::escape(staged_path.to_string_lossy().as_ref());\n    for pattern in expand_braces(pattern) {\n        validate_flight_relative_path(&pattern)?;\n        let rooted_pattern = Path::new(&escaped_root)\n            .join(Path::new(&pattern))\n            .to_string_lossy()\n            .to_string();\n        for path in glob::glob_with(\n            &rooted_pattern,\n            glob::MatchOptions {\n                require_literal_separator: true,\n                ..Default::default()\n            },\n        )\n        .wrap_err_with(|| format!(\"brew-cask: invalid structured flight glob '{pattern}'\"))?\n        {\n            let path = path?;\n            if !path.starts_with(staged_path) {\n                bail!(\n                    \"brew-cask: structured flight glob '{}' matched outside staged path\",\n                    pattern\n                );\n            }\n            matches.push(path);\n        }\n    }\n    matches.sort();\n    matches.dedup();\n    Ok(matches)\n}\n\nfn is_flight_glob(path: &str) -> bool {\n    path.chars()\n        .any(|c| matches!(c, '*' | '?' | '[' | ']' | '{' | '}'))\n}\n\nfn resolve_flight_path(staged_path: &Path, path: &FlightPath) -> Result<PathBuf> {","sourceCodeStart":3925,"sourceCodeEnd":3961,"githubUrl":"https://github.com/jdx/mise/blob/6f52dcdf99e282ef7a7db68c81301fa4618d0f79/src/system/packages/brew/cask.rs#L3925-L3961","documentation":"Every path matched by a flight glob must stay under the staged cask directory. After expansion, each match is checked with `path.starts_with(staged_path)` and any escape aborts the install, enforcing the sandbox invariant that structured steps touch only staged files. In practice this fires when the staged root contains symlinked components or the pattern yields a lexically different root than the checker expects.","triggerScenarios":"The staged root is reached through a symlink the glob resolves differently than the prefix check expects (e.g. /tmp versus /private/tmp on macOS); patterns containing `..` or brace expansions producing rooted variants; a corrupted or adversarial cask stanza.","commonSituations":"A custom staging directory configured under a symlinked path; the staging directory moved between download and install; a hand-edited stanza with traversal segments.","solutions":["Ensure the staged path used for globbing is the same real, non-symlinked root the matches are checked against (canonicalize the staged root)","Remove `..` and absolute segments from glob patterns in the cask definition","Re-stage the cask so staging metadata matches the on-disk location"],"exampleFix":"// before\nlet staged = \"/tmp/caskroom/myapp\"; // /tmp is a symlink on macOS\n// after\nlet staged = std::fs::canonicalize(\"/tmp/caskroom/myapp\")?;","handlingStrategy":"validation","validationCode":"// Canonicalize the staged root before any glob expansion so matches stay comparable\nlet staged_real = std::fs::canonicalize(staged_path)?;\nfor m in expand_staged_glob(&staged_real, pattern)? {\n    debug_assert!(m.starts_with(&staged_real), \"glob escaped staged root: {}\", m.display());\n}","typeGuard":null,"tryCatchPattern":"if err.to_string().contains(\"matched outside staged path\") {\n    // re-stage the cask; this indicates a symlinked/moved staging root, not a retryable error\n}","preventionTips":["Avoid configuring the staging directory under symlinked paths like /tmp on macOS","Reject glob patterns containing `..` at parse time","Treat this error as a sandbox violation: fix the environment, never widen the check"],"tags":["brew-cask","glob","sandbox","path-traversal"],"backgroundTag":"path-traversal-detected","analyzedSha":"6f52dcdf99e282ef7a7db68c81301fa4618d0f79","analyzedAt":"2026-08-22T10:14:23.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}