denoland/deno · critical

refusing zip entry with unsafe path

Error message

refusing zip entry with unsafe path: {}

What it means

Thrown while extracting a LAUFEY Windows runtime .zip when `entry.enclosed_name()` returns None — the zip crate rejects names that are absolute, contain drive labels (C:\), or have `..` components, i.e. a zip-slip attempt (or a pathologically weird archive the tool refuses on principle). The extractor iterates entries manually instead of using archive.extract precisely to enforce this.

Solutions

  1. Do not extract the zip by hand or with other tools — keep it quarantined.
  2. List entries to confirm: `unzip -l <archive>` and look for absolute/`..` names.
  3. Clear the cache and retry once; if reproducible, report to the deno/laufey maintainers with the archive name.
Defensive patterns

Strategy: try-catch

Try / catch

# Abort and report; never retry with permissive tools
if deno desktop main.ts 2>&1 | grep -q "refusing zip entry with unsafe path"; then
  echo "SECURITY: zip-slip attempt in runtime archive — report upstream" >&2; exit 2
fi

Prevention

When it happens

Trigger: A zip entry named `../evil.dll`, `C:\Windows\system32\x.dll`, or `/abs/path`; archives built with non-normalized paths (`a/../b`); an upstream release accidentally packaged with absolute paths.

Common situations: Practically unreachable in normal use because the archive was SHA-256-verified against the pinned release first — a hit means the pinned release itself is malicious or mis-packaged, or the cache was corrupted post-verification.

Related errors


AI-assisted analysis of denoland/deno@f7822238ca (2026-08-20). Data as JSON: /api/errors/068c742e6dcc68a7. Report an issue: GitHub.

Appendix: source

Thrown at cli/tools/desktop.rs:2273

          let mut perms = meta.permissions();
          perms.set_mode(safe);
          let _ = std::fs::set_permissions(&dest_path, perms);
        }
      }
    }
  } else if name.ends_with(".zip") {
    let mut archive = zip::ZipArchive::new(std::io::Cursor::new(data))?;
    // Iterate entries manually rather than `archive.extract(dest)`: that
    // helper has the same shape as the tar `unpack` we deliberately
    // avoided (no perm masking; no defence-in-depth against zip-slip
    // beyond the crate's own checks). Treat the archive as untrusted.
    for i in 0..archive.len() {
      let mut entry = archive.by_index(i)?;
      // `enclosed_name` rejects drive labels, absolute paths and `..`
      // components. Anything that fails this check is a zip-slip attempt
      // (or a legitimately weird archive we don't want to handle).
      let Some(rel_path) = entry.enclosed_name() else {
        bail!("refusing zip entry with unsafe path: {}", entry.name());
      };
      // Defence in depth — re-check the components ourselves.
      if rel_path.components().any(|c| {
        matches!(
          c,
          std::path::Component::ParentDir | std::path::Component::RootDir
        )
      }) {
        bail!(
          "refusing zip entry with traversal path: {}",
          rel_path.display()
        );
      }
      // Refuse symlinks: with prior entries already extracted, a
      // symlink-then-write pair is the standard zip-slip-via-symlink
      // escape, and LAUFEY Windows archives have no legitimate need for
      // them.
      if entry.is_symlink() {

View on GitHub (pinned to f7822238ca)