astrid-runtime/astrid · critical

malicious shuttle detected: invalid path '{}'

Error message

malicious shuttle detected: invalid path '{}'

What it means

`unpack` validates each archive entry's path before writing anything to disk, rejecting absolute paths and any path containing `..` components. This is a zip-slip/path-traversal defense: a crafted `.shuttle` could otherwise write files outside the extraction directory.

Source

Thrown at crates/astrid-cli/src/commands/distro/shuttle.rs:188

    let tar = flate2::read::GzDecoder::new(tar_gz);
    let mut archive = tar::Archive::new(tar);

    for entry in archive
        .entries()
        .context("failed to read shuttle entries (truncated or not a gzip tar?)")?
    {
        let mut entry = entry.context("failed to read shuttle entry (truncated archive?)")?;
        let entry_path = entry
            .path()
            .context("invalid path in shuttle")?
            .into_owned();

        if entry_path.is_absolute()
            || entry_path
                .components()
                .any(|c| matches!(c, Component::ParentDir))
        {
            bail!(
                "malicious shuttle detected: invalid path '{}'",
                entry_path.display()
            );
        }

        let et = entry.header().entry_type();
        if et.is_symlink() || et.is_hard_link() {
            bail!(
                "malicious shuttle detected: links are not allowed ('{}')",
                entry_path.display()
            );
        }
        // Skip directory entries — parents are created as needed below.
        if et.is_dir() {
            continue;
        }
        // Everything that survives to here must be an ordinary file. Device
        // nodes, FIFOs, sockets, and any other special entry type are

View on GitHub (pinned to affd8760f4)

Solutions

  1. Do not install this shuttle — treat it as malicious or corrupt and obtain it from a trusted source.
  2. Re-pack the capsule with relative, `..`-free paths using the project's `pack` command.
  3. If you control the producer, fix the packing code to sanitize member paths (e.g. strip leading `/` and reject `..`).
Defensive patterns

Strategy: validation

Validate before calling

fn entry_path_is_safe(p: &str) -> bool {
    let path = std::path::Path::new(p);
    !path.is_absolute()
        && !path.components().any(|c| matches!(c, std::path::Component::ParentDir))
}

Type guard

fn is_safe_member(p: &Path) -> bool {
    p.is_relative() && !p.components().any(|c| matches!(c, Component::ParentDir))
}

Try / catch

if let Err(e) = shuttle::unpack(archive, dest) {
    if e.to_string().contains("invalid path") {
        eprintln!("rejecting malicious archive: {e}");
        // do not retry; discard the archive
    }
}

Prevention

When it happens

Trigger: Calling `shuttle::unpack` on an archive whose member header path is absolute (e.g. `/etc/passwd`) or contains a parent-directory component (e.g. `../../evil`). Detected by the malicious-entry tests.

Common situations: Installing a shuttle downloaded from an untrusted source; an archive produced by a buggy or tampered packer that stored raw absolute paths.

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 astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/d33546534f8dd486. Report an issue: GitHub.