{"record":{"id":"dbeb324b49cc04df","repo":"astrid-runtime/astrid","slug":"durable-capsule-archive-contains-unsafe-path","errorCode":null,"errorMessage":"durable capsule archive contains unsafe path","messagePattern":"durable capsule archive contains unsafe path","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/astrid-capsule-install/src/storage.rs","lineNumber":372,"sourceCode":"}\n\nfn read_archive_files(archive_bytes: &[u8]) -> anyhow::Result<ArchiveInventory> {\n    let decoder = flate2::read::GzDecoder::new(Cursor::new(archive_bytes));\n    let mut archive = tar::Archive::new(decoder);\n    let mut files = std::collections::BTreeMap::new();\n    let mut directories = std::collections::BTreeSet::new();\n    for entry in archive.entries().context(\"read durable capsule archive\")? {\n        let mut entry = entry.context(\"read durable capsule archive entry\")?;\n        let path = entry.path().context(\"read durable capsule archive path\")?;\n        if path.is_absolute()\n            || path.components().any(|component| {\n                matches!(\n                    component,\n                    std::path::Component::ParentDir | std::path::Component::RootDir\n                )\n            })\n        {\n            bail!(\"durable capsule archive contains unsafe path\");\n        }\n\n        let entry_type = entry.header().entry_type();\n        if !entry_type.is_dir() && !entry_type.is_file() {\n            bail!(\"durable capsule archive contains a link or special file\");\n        }\n\n        let name = path\n            .to_str()\n            .ok_or_else(|| anyhow::anyhow!(\"durable capsule archive path is not UTF-8\"))?\n            .replace('\\\\', \"/\");\n        if files.contains_key(&name) || directories.contains(&name) {\n            bail!(\"durable capsule archive contains duplicate path {name}\");\n        }\n        if entry_type.is_dir() {\n            if !directories.insert(name) {\n                bail!(\"durable capsule archive contains duplicate directory path\");\n            }","sourceCodeStart":354,"sourceCodeEnd":390,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/storage.rs#L354-L390","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Rebuild the archive with relative, traversal-free paths (strip leading '/' and '..' components at creation).","Reject or quarantine the offending archive; do not attempt to sanitize it silently.","Audit the archive-generation tooling for absolute-path leakage.","Verify archive provenance/manifest digest to detect tampering before reading."],"exampleFix":"// before: archiving with an absolute path\nlet name = entry_path.to_string_lossy(); // \"/abs/dir/file\"\n// after: make the path relative before writing to the tar\nlet name = entry_path.strip_prefix(source_dir)?.to_string_lossy().to_string();","handlingStrategy":"validation","validationCode":"fn entry_path_is_safe(p: &std::path::Path) -> bool {\n    !p.is_absolute()\n        && !p.components().any(|c| matches!(\n            c,\n            std::path::Component::ParentDir | std::path::Component::RootDir\n        ))\n}","typeGuard":"fn is_relative_safe(p: &std::path::Path) -> bool {\n    p.is_relative() && p.components().all(|c| matches!(c, std::path::Component::Normal(_)))\n}","tryCatchPattern":"match read_verified_durable_package_for_owner(&store, owner, id).await {\n    Ok(pkg) => pkg,\n    Err(e) if e.to_string().contains(\"unsafe path\") => {\n        // reject/quarantine the archive; rebuild with relative paths\n    },\n    Err(e) => return Err(e),\n}","preventionTips":["Always strip the source-dir prefix so tar entries are relative.","Scan archives with tar -tf or a library check for absolute/'..' paths before publishing.","Never accept capsule archives from untrusted sources without the manifest digest check."],"tags":["security","path-traversal","archive","tar"],"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-14T11:17:12.474Z"}