{"record":{"id":"9c8b7a2ebb98f458","repo":"astrid-runtime/astrid","slug":"materialization-path-escaped-destination","errorCode":null,"errorMessage":"materialization path escaped destination","messagePattern":"materialization path escaped destination","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-capsule-install/src/storage.rs","lineNumber":612,"sourceCode":"        }\n        let mut bytes = Vec::new();\n        entry\n            .read_to_end(&mut bytes)\n            .context(\"read durable capsule archive file\")?;\n        fs::write(&output, bytes)\n            .with_context(|| format!(\"write materialized file {}\", output.display()))?;\n    }\n    fs::write(destination.join(\"meta.json\"), &package.metadata)\n        .context(\"write materialized capsule metadata\")?;\n    fs::write(destination.join(\"authority.json\"), &package.authority)\n        .context(\"write materialized capsule authority\")?;\n    Ok(())\n}\n\nfn reject_symlink_ancestors(root: &Path, path: &Path) -> anyhow::Result<()> {\n    let relative = path\n        .strip_prefix(root)\n        .map_err(|_| anyhow::anyhow!(\"materialization path escaped destination\"))?;\n    let mut current = root.to_path_buf();\n    for component in relative.components() {\n        current.push(component.as_os_str());\n        if fs::symlink_metadata(&current).is_ok_and(|metadata| metadata.file_type().is_symlink()) {\n            bail!(\"materialization parent is a symlink: {}\", current.display());\n        }\n    }\n    Ok(())\n}\n\nmod leftover;\nmod migration;\n\npub use leftover::retire_unmatched_legacy_authority_receipts;\npub use migration::{\n    LegacyCapsuleAuthorityReceipt, LegacyCapsuleMigrationReport, LegacyEnvSecretImportStatus,\n    legacy_capsule_authority_status, legacy_env_secret_import_status, migrate_all_native_capsules,\n    migrate_all_native_capsules_with_report, migrate_native_capsules,","sourceCodeStart":594,"sourceCodeEnd":630,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/storage.rs#L594-L630","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Rebuild the capsule package ensuring all member paths are relative and stay inside the archive root; republish.","Check the package source for entries with ../ or absolute paths and strip/normalize them before materialization.","Only materialize packages from trusted sources and verify archive member paths before calling materialize_capsule_package."],"exampleFix":"// before: archive members recorded as \"../../etc/evil\"\n// after: build the package with root-relative paths, e.g. \"lib/component.wasm\"","handlingStrategy":"validation","validationCode":"fn is_safe_member_path(root: &Path, member: &str) -> bool {\n    let p = Path::new(member);\n    !p.is_absolute() && p.components().all(|c| !matches!(c, std::path::Component::ParentDir))\n        && Path::new(root).join(p).starts_with(root)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only materialize capsules from trusted, signed sources","Pre-scan archive member paths for ../ or absolute entries before extraction","Normalize paths when building packages so every member is root-relative"],"tags":["security","path-traversal","materialization"],"backgroundTag":"path-traversal-blocked","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}