{"record":{"id":"688b5289f68d8c4f","repo":"astrid-runtime/astrid","slug":"unsafe-archive-path-path","errorCode":null,"errorMessage":"unsafe archive path '{path}'","messagePattern":"unsafe archive path '(.+?)'","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/commands/distro/shuttle.rs","lineNumber":258,"sourceCode":"\n/// The archive-relative path of a capsule member: `capsules/<name>.capsule`.\npub(crate) fn capsule_member_path(name: &str) -> String {\n    format!(\"{CAPSULES_DIR}/{name}.capsule\")\n}\n\n/// The on-disk path of a capsule inside an unpacked mirror.\npub(crate) fn capsule_mirror_path(mirror: &Path, name: &str) -> PathBuf {\n    mirror.join(CAPSULES_DIR).join(format!(\"{name}.capsule\"))\n}\n\n/// Reject archive paths that are absolute, contain `..`, or are empty.\nfn validate_archive_path(path: &str) -> anyhow::Result<()> {\n    if path.is_empty() {\n        bail!(\"empty archive path\");\n    }\n    let p = Path::new(path);\n    if p.is_absolute() || p.components().any(|c| matches!(c, Component::ParentDir)) {\n        bail!(\"unsafe archive path '{path}'\");\n    }\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn sample_entries() -> Vec<ShuttleEntry> {\n        vec![\n            ShuttleEntry {\n                path: SIG_NAME.to_string(),\n                content: ShuttleContent::Bytes(b\"deadbeef\".to_vec()),\n            },\n            ShuttleEntry {\n                path: MANIFEST_NAME.to_string(),\n                content: ShuttleContent::Bytes(b\"schema-version = 1\\n\".to_vec()),\n            },","sourceCodeStart":240,"sourceCodeEnd":276,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/distro/shuttle.rs#L240-L276","documentation":"`validate_archive_path` rejects archive paths that are absolute or contain `..` components, preventing capsules from being written to arbitrary filesystem locations. Only relative, traversal-free paths are accepted.","triggerScenarios":"Calling `pack` with an output path like `/tmp/foo.capsule` or `../foo.capsule`; `validate_archive_path` bails before the archive is created.","commonSituations":"Users passing an absolute path out of habit (`--out $(pwd)/x.capsule`); scripts built on a different machine using `..`-relative paths; path built by string concatenation from user input.","solutions":["Use a relative path without `..`, e.g. `--out my.capsule` or `--out out/my.capsule`.","`cd` to the intended output directory and pass a bare filename.","Omit the path so pack writes to the default mirror location `<name>.capsule`."],"exampleFix":"// before\n$ astrid distro pack --out /tmp/x.capsule\n// after\n$ cd /tmp && astrid distro pack --out x.capsule","handlingStrategy":"validation","validationCode":"fn archive_path_is_safe(p: &str) -> bool {\n    let path = std::path::Path::new(p);\n    !path.is_absolute()\n        && !path.components().any(|c| matches!(c, std::path::Component::ParentDir))\n}","typeGuard":"fn is_safe_out_path(p: &str) -> bool {\n    let q = Path::new(p);\n    q.is_relative() && !q.components().any(|c| matches!(c, Component::ParentDir))\n}","tryCatchPattern":"match pack(&staging, out) {\n    Err(e) if e.to_string().contains(\"unsafe archive path\") => {\n        eprintln!(\"use a relative path without '..': {e}\");\n    }\n    other => other?,\n}","preventionTips":["Pass bare relative filenames for --out","cd into the target directory instead of using absolute paths","Never interpolate user input directly into the output path"],"tags":["cli","validation","path-traversal","path"],"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"}