zeroclaw-labs/zeroclaw · critical · anyhow::Error
plugin archive contains unsafe path: {raw_name}
Error message
plugin archive contains unsafe path: {raw_name} What it means
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.
Source
Thrown at src/plugin_registry.rs:211
}
std::fs::create_dir_all(parent)?;
let mut out = File::create(&out_path)?;
copy_zip_entry_capped(
&mut file,
&mut out,
&mut extracted_bytes,
max_extracted_bytes,
)?;
}
Ok(dest.to_path_buf())
}
fn enclosed_zip_path<R>(raw_name: &str, file: &zip::read::ZipFile<'_, R>) -> Result<PathBuf>
where
R: Read,
{
if is_unsafe_zip_entry_name(raw_name) {
bail!("plugin archive contains unsafe path: {raw_name}");
}
file.enclosed_name().ok_or_else(|| {
anyhow::Error::msg(format!("plugin archive contains unsafe path: {raw_name}"))
})
}
fn is_unsafe_zip_entry_name(raw_name: &str) -> bool {
raw_name.starts_with('/')
|| raw_name.starts_with('\\')
|| has_windows_drive_prefix(raw_name)
|| raw_name
.split(['/', '\\'])
.any(|component| component == "..")
}
fn has_windows_drive_prefix(raw_name: &str) -> bool {
let bytes = raw_name.as_bytes();
bytes.len() >= 2 && bytes[1] == b':' && bytes[0].is_ascii_alphabetic()View on GitHub (pinned to 88bb9c8533)
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)
Example fix
# before: entry name "/etc/zeroclaw/pwn.toml" or "../../escape.toml" # after: rebuild with relative, rooted paths cd plugin-root zip -r ../p-0.1.0.zip . # entries like "manifest.toml", "src/lib.rs"
Defensive patterns
Strategy: validation
Validate before calling
// Pre-scan entry names with the same two rules the guard applies:
for i in 0..archive.len() {
let f = archive.by_index(i)?;
let name = f.name();
if is_unsafe_zip_entry_name(name) || f.enclosed_name().is_none() {
anyhow::bail!("unsafe entry: {name}");
}
} Type guard
fn is_safe_entry_name(f: &zip::read::ZipFile<'_, impl Read>) -> bool {
!is_unsafe_zip_entry_name(f.name()) && f.enclosed_name().is_some()
} Try / catch
// On 'unsafe path': abort install immediately, keep the archive for analysis, // and report to the registry — this is the zip-slip tripwire, never bypass it // by extracting manually.
Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- plugin archive exceeds extracted size limit of {max_extracte
- Lark/Feishu marker target resolves outside workspace_dir
- attachment path {} canonicalizes to {} which escapes workspa
- attachment path {} escapes workspace {}
- manifest exec binary escapes plugin directory: {} is not und
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/b8c86d528da8a511.
Report an issue: GitHub.