astrid-runtime/astrid · critical

durable capsule archive contains unsafe path

Error message

durable capsule archive contains unsafe path

What it means

While inventorying the tar archive, read_archive_files encountered an entry whose path is absolute or contains a ParentDir (..) or RootDir component. This is a path-traversal guard: such entries could escape the intended extraction root, so the library refuses to process the archive at all.

Source

Thrown at crates/astrid-capsule-install/src/storage.rs:372

}

fn read_archive_files(archive_bytes: &[u8]) -> anyhow::Result<ArchiveInventory> {
    let decoder = flate2::read::GzDecoder::new(Cursor::new(archive_bytes));
    let mut archive = tar::Archive::new(decoder);
    let mut files = std::collections::BTreeMap::new();
    let mut directories = std::collections::BTreeSet::new();
    for entry in archive.entries().context("read durable capsule archive")? {
        let mut entry = entry.context("read durable capsule archive entry")?;
        let path = entry.path().context("read durable capsule archive path")?;
        if path.is_absolute()
            || path.components().any(|component| {
                matches!(
                    component,
                    std::path::Component::ParentDir | std::path::Component::RootDir
                )
            })
        {
            bail!("durable capsule archive contains unsafe path");
        }

        let entry_type = entry.header().entry_type();
        if !entry_type.is_dir() && !entry_type.is_file() {
            bail!("durable capsule archive contains a link or special file");
        }

        let name = path
            .to_str()
            .ok_or_else(|| anyhow::anyhow!("durable capsule archive path is not UTF-8"))?
            .replace('\\', "/");
        if files.contains_key(&name) || directories.contains(&name) {
            bail!("durable capsule archive contains duplicate path {name}");
        }
        if entry_type.is_dir() {
            if !directories.insert(name) {
                bail!("durable capsule archive contains duplicate directory path");
            }

View on GitHub (pinned to affd8760f4)

Solutions

  1. Rebuild the archive with relative, traversal-free paths (strip leading '/' and '..' components at creation).
  2. Reject or quarantine the offending archive; do not attempt to sanitize it silently.
  3. Audit the archive-generation tooling for absolute-path leakage.
  4. Verify archive provenance/manifest digest to detect tampering before reading.

Example fix

// before: archiving with an absolute path
let name = entry_path.to_string_lossy(); // "/abs/dir/file"
// after: make the path relative before writing to the tar
let name = entry_path.strip_prefix(source_dir)?.to_string_lossy().to_string();
Defensive patterns

Strategy: validation

Validate before calling

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

Type guard

fn is_relative_safe(p: &std::path::Path) -> bool {
    p.is_relative() && p.components().all(|c| matches!(c, std::path::Component::Normal(_)))
}

Try / catch

match read_verified_durable_package_for_owner(&store, owner, id).await {
    Ok(pkg) => pkg,
    Err(e) if e.to_string().contains("unsafe path") => {
        // reject/quarantine the archive; rebuild with relative paths
    },
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: read_archive_files (via read_verified_durable_package_for_owner) on a tar.gz containing entries like "/etc/passwd", "../../escape", or paths with leading root components.

Common situations: Building the tar on Windows or with scripts that emit absolute paths; malicious or tampered archives; archiving with tools that preserve leading slashes; hand-crafted tar files in tests.

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/dbeb324b49cc04df. Report an issue: GitHub.