astrid-runtime/astrid · error

materialization path escaped destination

Error message

materialization path escaped destination

What it means

Thrown by reject_symlink_ancestors when the path being materialized cannot be expressed as a path relative to the destination root, meaning the materialization path points outside the destination directory. The library refuses to write outside the destination to prevent path traversal attacks.

Solutions

  1. Rebuild the capsule package ensuring all member paths are relative and stay inside the archive root; republish.
  2. Check the package source for entries with ../ or absolute paths and strip/normalize them before materialization.
  3. Only materialize packages from trusted sources and verify archive member paths before calling materialize_capsule_package.

Example fix

// before: archive members recorded as "../../etc/evil"
// after: build the package with root-relative paths, e.g. "lib/component.wasm"
Defensive patterns

Strategy: validation

Validate before calling

fn is_safe_member_path(root: &Path, member: &str) -> bool {
    let p = Path::new(member);
    !p.is_absolute() && p.components().all(|c| !matches!(c, std::path::Component::ParentDir))
        && Path::new(root).join(p).starts_with(root)
}

Prevention

When it happens

Trigger: materialize_capsule_package receives a package whose internal entry paths, joined onto the destination root, are not under that root (e.g. paths containing ../ segments or absolute entries), so path.strip_prefix(root) fails.

Common situations: Extracting a maliciously crafted capsule archive with path-escaping entries ('zip-slip' style archives); archives built on other platforms or by buggy tooling that stored absolute/parent-relative member names.

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

Appendix: source

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

        }
        let mut bytes = Vec::new();
        entry
            .read_to_end(&mut bytes)
            .context("read durable capsule archive file")?;
        fs::write(&output, bytes)
            .with_context(|| format!("write materialized file {}", output.display()))?;
    }
    fs::write(destination.join("meta.json"), &package.metadata)
        .context("write materialized capsule metadata")?;
    fs::write(destination.join("authority.json"), &package.authority)
        .context("write materialized capsule authority")?;
    Ok(())
}

fn reject_symlink_ancestors(root: &Path, path: &Path) -> anyhow::Result<()> {
    let relative = path
        .strip_prefix(root)
        .map_err(|_| anyhow::anyhow!("materialization path escaped destination"))?;
    let mut current = root.to_path_buf();
    for component in relative.components() {
        current.push(component.as_os_str());
        if fs::symlink_metadata(&current).is_ok_and(|metadata| metadata.file_type().is_symlink()) {
            bail!("materialization parent is a symlink: {}", current.display());
        }
    }
    Ok(())
}

mod leftover;
mod migration;

pub use leftover::retire_unmatched_legacy_authority_receipts;
pub use migration::{
    LegacyCapsuleAuthorityReceipt, LegacyCapsuleMigrationReport, LegacyEnvSecretImportStatus,
    legacy_capsule_authority_status, legacy_env_secret_import_status, migrate_all_native_capsules,
    migrate_all_native_capsules_with_report, migrate_native_capsules,

View on GitHub (pinned to affd8760f4)