{"record":{"id":"98e97b7769b8ec0b","repo":"astrid-runtime/astrid","slug":"durable-capsule-id-has-unsafe-wit-metadata-path","errorCode":null,"errorMessage":"durable capsule {id} has unsafe WIT metadata path {relative}","messagePattern":"durable capsule (.+?) has unsafe WIT metadata path (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-capsule-install/src/storage.rs","lineNumber":221,"sourceCode":"    id: &str,\n    metadata: &CapsuleMeta,\n    files: &std::collections::BTreeMap<String, Vec<u8>>,\n) -> anyhow::Result<()> {\n    let mut expected = std::collections::BTreeSet::new();\n    for (relative, pin) in &metadata.wit_files {\n        let relative_path = Path::new(relative);\n        if relative_path.is_absolute()\n            || relative_path.components().any(|component| {\n                matches!(\n                    component,\n                    std::path::Component::ParentDir | std::path::Component::RootDir\n                )\n            })\n            || relative_path\n                .components()\n                .any(|component| !matches!(component, std::path::Component::Normal(_)))\n        {\n            bail!(\"durable capsule {id} has unsafe WIT metadata path {relative}\");\n        }\n        let key = format!(\"wit/{relative}\");\n        let Some(bytes) = files.get(&key) else {\n            bail!(\"durable capsule {id} is missing WIT file {relative}\");\n        };\n        if !is_hex_digest(pin) || blake3::hash(bytes).to_hex().as_str() != pin {\n            bail!(\"durable capsule {id} WIT digest mismatch for {relative}\");\n        }\n        expected.insert(key);\n    }\n    for key in files.keys().filter(|key| key.starts_with(\"wit/\")) {\n        if Path::new(key)\n            .extension()\n            .is_some_and(|extension| extension.eq_ignore_ascii_case(\"wit\"))\n            && !expected.contains(key)\n        {\n            bail!(\"durable capsule {id} has an unpinned WIT file {key}\");\n        }","sourceCodeStart":203,"sourceCodeEnd":239,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/storage.rs#L203-L239","documentation":"verify_wit_files validates the WIT metadata of a durable capsule package read from principal storage. Every relative path in metadata.wit_files must consist only of Normal components — absolute paths, `..`, `.` or root components are rejected as unsafe before the file is looked up. This prevents a tampered metadata record from addressing files outside the capsule's wit/ namespace via traversal.","triggerScenarios":"Calling read_verified_durable_package_for_owner on a durable capsule whose CapsuleMeta.wit_files keys contain an absolute path or non-Normal components (`..`, `.`, root). Usually indicates a corrupted or hand-edited metadata record, or a bug in the code that wrote wit_files.","commonSituations":"Manually editing the durable store's metadata JSON; restoring a store from a partially migrated backup; a published capsule whose metadata was generated by an old/buggy packager embedding absolute WIT paths.","solutions":["Inspect the capsule's metadata (wit_files keys) and correct any absolute or `..` paths to plain relative names like `api.wit` or `deps/types.wit`.","Reinstall or republish the capsule so metadata is regenerated by the tooling.","If the store was hand-edited or restored, rebuild it from authoritative capsule sources.","Report/fix the packager that emitted non-relative WIT paths."],"exampleFix":"// before: unsafe metadata entry\nwit_files: {\"/abs/api.wit\": \"<pin>\"}\n// after: normal relative path\nwit_files: {\"api.wit\": \"<blake3-hex-pin>\"}","handlingStrategy":"validation","validationCode":"fn wit_paths_are_safe(wit_files: &std::collections::BTreeMap<String, String>) -> bool {\n    wit_files.keys().all(|rel| {\n        let p = std::path::Path::new(rel);\n        !p.is_absolute()\n            && p.components().all(|c| matches!(c, std::path::Component::Normal(_)))\n    })\n}","typeGuard":"fn is_normal_relative(rel: &str) -> bool {\n    let p = std::path::Path::new(rel);\n    !p.is_absolute() && p.components().all(|c| matches!(c, std::path::Component::Normal(_)))\n}","tryCatchPattern":"match read_verified_durable_package_for_owner(id) {\n    Err(e) if e.to_string().contains(\"unsafe WIT metadata path\") => {\n        // metadata record is corrupt/tampered: reinstall or republish the capsule\n        reinstall_capsule(id)?;\n    }\n    other => other?,\n}","preventionTips":["Never hand-edit durable store metadata files","Keep WIT paths as plain relative names (no leading /, no ..)","Rebuild capsules from source after any restore/migration instead of patching metadata","Validate wit_files keys when generating metadata programmatically"],"tags":["security","path-traversal","metadata","validation"],"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-14T05:17:10.506Z"}