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

  1. Do not install this archive — a traversal entry is either malicious or grossly malformed; report it to the registry maintainer
  2. 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 .`)
  3. Inspect the offending name in the error and locate it via `unzip -l` to confirm it is a packing bug, not tampering
  4. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/b8c86d528da8a511. Report an issue: GitHub.