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
- Repack the archive with clean relative entry names (no leading /, no .. segments) and without symlink entries
- 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)
- 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
- Scan archives for .. and absolute entry names before extraction
- Repack archives that contain symlink entries as plain files
- Only accept archives from trusted sources; treat failures as malicious-input signals
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
- archive entry escapes destination
- invalid archive entry
- access to sensitive workspace file is forbidden
- asset path is sensitive
- invalid archive entry path
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)