{"record":{"id":"b8c86d528da8a511","repo":"zeroclaw-labs/zeroclaw","slug":"plugin-archive-contains-unsafe-path-raw-name","errorCode":null,"errorMessage":"plugin archive contains unsafe path: {raw_name}","messagePattern":"plugin archive contains unsafe path: (.+?)","errorType":"console","errorClass":"anyhow::Error","httpStatus":null,"severity":"critical","filePath":"src/plugin_registry.rs","lineNumber":211,"sourceCode":"        }\n        std::fs::create_dir_all(parent)?;\n        let mut out = File::create(&out_path)?;\n        copy_zip_entry_capped(\n            &mut file,\n            &mut out,\n            &mut extracted_bytes,\n            max_extracted_bytes,\n        )?;\n    }\n    Ok(dest.to_path_buf())\n}\n\nfn enclosed_zip_path<R>(raw_name: &str, file: &zip::read::ZipFile<'_, R>) -> Result<PathBuf>\nwhere\n    R: Read,\n{\n    if is_unsafe_zip_entry_name(raw_name) {\n        bail!(\"plugin archive contains unsafe path: {raw_name}\");\n    }\n    file.enclosed_name().ok_or_else(|| {\n        anyhow::Error::msg(format!(\"plugin archive contains unsafe path: {raw_name}\"))\n    })\n}\n\nfn is_unsafe_zip_entry_name(raw_name: &str) -> bool {\n    raw_name.starts_with('/')\n        || raw_name.starts_with('\\\\')\n        || has_windows_drive_prefix(raw_name)\n        || raw_name\n            .split(['/', '\\\\'])\n            .any(|component| component == \"..\")\n}\n\nfn has_windows_drive_prefix(raw_name: &str) -> bool {\n    let bytes = raw_name.as_bytes();\n    bytes.len() >= 2 && bytes[1] == b':' && bytes[0].is_ascii_alphabetic()","sourceCodeStart":193,"sourceCodeEnd":229,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/src/plugin_registry.rs#L193-L229","documentation":"Zip-slip defense: enclosed_zip_path rejects entry names that are unsafe (is_unsafe_zip_entry_name — absolute paths, `..` traversal, and similar) or that the zip crate's enclosed_name() cannot confine under the extraction root. The bail fires before any file is written, naming the offending raw path.","triggerScenarios":"Extracting a plugin archive containing an entry like \"../../.ssh/authorized_keys\", \"/etc/passwd\", or a symlink-ish/traversal name that escapes dest. Triggered from extract_zip_safe_with_limit on install.","commonSituations":"A malicious third-party plugin attempting path traversal onto the host; an archive built with absolute paths by a misconfigured packer (some tools store absolute names if given them); archives re-packed on Windows with drive-prefixed entries.","solutions":["Do not install this archive — a traversal entry is either malicious or grossly malformed; report it to the registry maintainer","If it is your own plugin, rebuild the zip from inside the plugin root with relative paths only (`cd plugin-root && zip -r ../p.zip .`)","Inspect the offending name in the error and locate it via `unzip -l` to confirm it is a packing bug, not tampering","Prefer registry entries with sha256 digests so tampered archives fail earlier at the checksum (error 1411)"],"exampleFix":"# before: entry name \"/etc/zeroclaw/pwn.toml\" or \"../../escape.toml\"\n# after: rebuild with relative, rooted paths\ncd plugin-root\nzip -r ../p-0.1.0.zip .          # entries like \"manifest.toml\", \"src/lib.rs\"","handlingStrategy":"validation","validationCode":"// Pre-scan entry names with the same two rules the guard applies:\nfor i in 0..archive.len() {\n    let f = archive.by_index(i)?;\n    let name = f.name();\n    if is_unsafe_zip_entry_name(name) || f.enclosed_name().is_none() {\n        anyhow::bail!(\"unsafe entry: {name}\");\n    }\n}","typeGuard":"fn is_safe_entry_name(f: &zip::read::ZipFile<'_, impl Read>) -> bool {\n    !is_unsafe_zip_entry_name(f.name()) && f.enclosed_name().is_some()\n}","tryCatchPattern":"// On 'unsafe path': abort install immediately, keep the archive for analysis,\n// and report to the registry — this is the zip-slip tripwire, never bypass it\n// by extracting manually.","preventionTips":["Only install plugins from trusted registries with pinned sha256","Build zips from inside the plugin root so all paths are relative","Lint archives for absolute or '..'-containing names in CI"],"tags":["plugin-registry","zip","path-traversal","zip-slip","security"],"backgroundTag":"zip-slip-path-traversal","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}