{"record":{"id":"9229a7ced604c332","repo":"jdx/mise","slug":"brew-cask-invalid-structured-flight-path-9229a7","errorCode":null,"errorMessage":"brew-cask: invalid structured flight path '{}'","messagePattern":"brew-cask: invalid structured flight path '(.+?)'","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask/flight.rs","lineNumber":1207,"sourceCode":"        .replace(\"{{staged_path}}\", &staged_path.to_string_lossy())\n        .replace(\"{{appdir}}\", &appdir.to_string_lossy());\n    if let Some(version) = version {\n        value = value.replace(\"{{version}}\", version);\n    }\n    if let Some(rest) = value.strip_prefix(\"~/\") {\n        value = crate::dirs::HOME.join(rest).to_string_lossy().to_string();\n    }\n    value\n}\n\npub(super) fn validate_flight_relative_path(path: &str) -> Result<()> {\n    let path = Path::new(path);\n    if path.is_absolute()\n        || path\n            .components()\n            .any(|component| matches!(component, Component::ParentDir))\n    {\n        bail!(\n            \"brew-cask: invalid structured flight path '{}'\",\n            path.display()\n        );\n    }\n    Ok(())\n}\n\npub(super) fn expand_braces(pattern: &str) -> Vec<String> {\n    let Some(start) = pattern.find('{') else {\n        return vec![pattern.to_string()];\n    };\n    let Some(end_offset) = pattern[start + 1..].find('}') else {\n        return vec![pattern.to_string()];\n    };\n    let end = start + 1 + end_offset;\n    let prefix = &pattern[..start];\n    let suffix = &pattern[end + 1..];\n    let mut expanded = Vec::new();","sourceCodeStart":1189,"sourceCodeEnd":1225,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/packages/brew/cask/flight.rs#L1189-L1225","documentation":"validate_flight_relative_path enforces that brew-cask 'flight' (staged artifact) paths stay relative and safe before they are joined to an extraction root. A path is rejected if it is absolute or contains a ParentDir (`..`) component, because either would let an archive or cask definition address files outside the staging directory (path traversal).","triggerScenarios":"Any brew-cask install that resolves a flight path via parse_flight_path, parse_permissions_flight_path, expand_staged_glob, resolve_flight_path, or resolve_flight_path_with_context when the cask's artifact/permission path is absolute (starts with '/') or contains a '..' component.","commonSituations":"Installing a hand-written or third-party cask whose app/binary/preflight stanza references '/Applications/...' or 'foo/../../bar'; a cask stanza edited to target outside the staged archive; corrupted or malicious cask JSON pulled from a nonstandard tap.","solutions":["Fix the cask's artifact path so it is relative to the extraction root and contains no '..' components (e.g. 'MyApp.app' instead of '/Applications/MyApp.app').","If targeting an absolute install location is intended, use the artifact's target/dest fields (which support absolute destinations) rather than the source flight path.","Inspect the offending cask definition/tap for tampering and re-pin to the upstream cask.","If this fires on a legitimate path, report/normalize the path before calling (strip leading slashes, resolve '..' lexically) and re-validate."],"exampleFix":"// before\nflight_path = \"/Applications/MyApp.app\"\n// after\nflight_path = \"MyApp.app\"  # relative to extraction root, no '..'","handlingStrategy":"validation","validationCode":"use std::path::{Path, Component};\nfn is_safe_flight_path(p: &str) -> bool {\n    let path = Path::new(p);\n    !path.is_absolute()\n        && !path.components().any(|c| matches!(c, Component::ParentDir))\n        && !p.is_empty()\n}\nif !is_safe_flight_path(user_path) {\n    return Err(format!(\"flight path must be relative and contain no '..': {user_path}\"));\n}","typeGuard":"fn safe_relative_path(p: &Path) -> Option<&Path> {\n    if p.is_absolute() || p.components().any(|c| matches!(c, Component::ParentDir)) { None } else { Some(p) }\n}","tryCatchPattern":null,"preventionTips":["Always define cask artifact sources relative to the archive root.","Never author cask stanzas with absolute source paths; use the target/dest field for absolute destinations.","Validate cask JSON with a linter that rejects '..' and absolute sources.","Treat third-party tap casks as untrusted input and review their paths."],"tags":["rust","brew","path-traversal","security","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"}