jdx/mise · error

brew-cask: invalid structured flight path

Error message

brew-cask: invalid structured flight path '{}'

What it means

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).

Solutions

  1. 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').
  2. 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.
  3. Inspect the offending cask definition/tap for tampering and re-pin to the upstream cask.
  4. If this fires on a legitimate path, report/normalize the path before calling (strip leading slashes, resolve '..' lexically) and re-validate.

Example fix

// before
flight_path = "/Applications/MyApp.app"
// after
flight_path = "MyApp.app"  # relative to extraction root, no '..'
Defensive patterns

Strategy: validation

Validate before calling

use std::path::{Path, Component};
fn is_safe_flight_path(p: &str) -> bool {
    let path = Path::new(p);
    !path.is_absolute()
        && !path.components().any(|c| matches!(c, Component::ParentDir))
        && !p.is_empty()
}
if !is_safe_flight_path(user_path) {
    return Err(format!("flight path must be relative and contain no '..': {user_path}"));
}

Type guard

fn safe_relative_path(p: &Path) -> Option<&Path> {
    if p.is_absolute() || p.components().any(|c| matches!(c, Component::ParentDir)) { None } else { Some(p) }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17). Data as JSON: /api/errors/9229a7ced604c332. Report an issue: GitHub.

Appendix: source

Thrown at src/system/packages/brew/cask/flight.rs:1207

        .replace("{{staged_path}}", &staged_path.to_string_lossy())
        .replace("{{appdir}}", &appdir.to_string_lossy());
    if let Some(version) = version {
        value = value.replace("{{version}}", version);
    }
    if let Some(rest) = value.strip_prefix("~/") {
        value = crate::dirs::HOME.join(rest).to_string_lossy().to_string();
    }
    value
}

pub(super) fn validate_flight_relative_path(path: &str) -> Result<()> {
    let path = Path::new(path);
    if path.is_absolute()
        || path
            .components()
            .any(|component| matches!(component, Component::ParentDir))
    {
        bail!(
            "brew-cask: invalid structured flight path '{}'",
            path.display()
        );
    }
    Ok(())
}

pub(super) fn expand_braces(pattern: &str) -> Vec<String> {
    let Some(start) = pattern.find('{') else {
        return vec![pattern.to_string()];
    };
    let Some(end_offset) = pattern[start + 1..].find('}') else {
        return vec![pattern.to_string()];
    };
    let end = start + 1 + end_offset;
    let prefix = &pattern[..start];
    let suffix = &pattern[end + 1..];
    let mut expanded = Vec::new();

View on GitHub (pinned to 533346cc37)