siyuan-note/siyuan · error

invalid archive entry

Error message

invalid archive entry [%s]

What it means

While extracting an archive, extractGuardedArchive validates each member name before writing: filepath.IsLocal must consider the slash-normalized name local (no absolute paths, no ".." components, not rooted) and the entry must not be a symlink. Either condition yields this error, blocking classic zip-slip and symlink-based archive attacks.

Solutions

  1. Repack the archive with clean relative entry names (no leading /, no .. segments) and without symlink entries
  2. If the archive legitimately contains symlinks, materialize them as regular files before zipping or extract with a tool outside the MCP guard set (within policy)
  3. Inspect the archive listing (unzip -l) to find the offending entry name and fix or drop it

Example fix

// before (zip contains entry "../evil.txt")
unzipHandler({"path": "payload.zip"}) // -> invalid archive entry
// after (repack with sanitized names)
zip -r clean.zip evil.txt  # entry name: evil.txt
unzipHandler({"path": "clean.zip"})
Defensive patterns

Strategy: validation

Validate before calling

for (const name of entryNames) {
  const norm = name.replace(/\\/g, '/');
  if (path.isAbsolute(norm) || norm.split('/').includes('..')) {
    throw new Error(`unsafe archive entry: ${name}`);
  }
}

Type guard

function isSafeEntryName(name) {
  const norm = name.replace(/\\/g, '/');
  return !norm.startsWith('/') && !norm.split('/').includes('..') && norm.length > 0;
}

Try / catch

try {
  await callMcpTool('unzip', { path: zipPath, dest });
} catch (e) {
  if (e.message.startsWith('invalid archive entry')) {
    // reject/quarantine the archive; do not attempt manual extraction
  }
}

Prevention

When it happens

Trigger: unzipHandler on an archive containing an entry named like "/etc/passwd", "../escape.txt", "a/../../x", a Windows-style "..\evil" name, or any entry whose mode includes os.ModeSymlink.

Common situations: Malicious or hand-crafted zips; archives produced on Windows with backslash traversal names (the code normalizes backslashes first); zips built with tools that store symlinks; legacy archives with rooted entry names.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/af7644082b0a1cd5. Report an issue: GitHub.

Appendix: source

Thrown at kernel/mcp/tools/unzip.go:123

	defer reader.Close()

	// 成员名与其最终输出路径成对保存,两次遍历使用同一份判定输入
	type archiveMember struct {
		name string
		path string
	}
	members := make([]archiveMember, len(reader.File))
	for i, entry := range reader.File {
		name := entry.Name
		if !utf8.ValidString(name) {
			// 与 Gulu 一致地解码 GB18030 条目名,避免解码前后的名称不一致导致校验被绕过
			if name, err = simplifiedchinese.GB18030.NewDecoder().String(name); err != nil {
				return err
			}
		}
		name = strings.ReplaceAll(name, "\\", "/")
		if !filepath.IsLocal(filepath.FromSlash(name)) || entry.Mode()&os.ModeSymlink != 0 {
			return fmt.Errorf("invalid archive entry [%s]", name)
		}
		members[i] = archiveMember{name: name, path: filepath.Join(destAbs, filepath.FromSlash(name))}
		if err = authorizeArchiveEntry(destAbs, members[i].path, members[i].name); err != nil {
			return err
		}
	}
	for i, entry := range reader.File {
		// 解压前再次授权,不复用预检阶段的判定结果
		if err = authorizeArchiveEntry(destAbs, members[i].path, members[i].name); err != nil {
			return err
		}
		if err = extractArchiveEntry(entry, members[i].path); err != nil {
			return err
		}
	}
	return nil
}

View on GitHub (pinned to 9f775e8a12)