siyuan-note/siyuan · error
archive entry escapes destination
Error message
archive entry escapes destination [%s]
What it means
authorizeArchiveEntry re-checks each extracted member's final output path after joining with the destination: filepath.Rel from destAbs must be local. If an entry's joined path escapes the destination directory (or Rel fails) it returns this error. This is the first of two containment checks — a second check re-verifies after symlink resolution.
Solutions
- Verify the destination directory passed to the unzip tool is a plain, real (non-symlink) directory inside the workspace
- Re-list the archive entries and remove any with .., absolute, or otherwise anomalous names, then repack
- If you need files elsewhere, extract into the intended directory directly instead of relying on entry paths to climb out
Example fix
// before (dest itself is a symlink)
dest = "/workspace/data/link-to-elsewhere"
unzipHandler({"path": "a.zip", "dest": dest}) // entry Rel escapes
// after
dest = "/workspace/data/extracted" // real directory
os.MkdirAll(dest, 0755)
unzipHandler({"path": "a.zip", "dest": dest}) Defensive patterns
Strategy: validation
Validate before calling
const destAbs = fs.realpathSync(path.resolve(dest));
if (!destAbs.startsWith(WORKSPACE_DIR + path.sep)) {
throw new Error('destination must be a real directory inside the workspace');
} Type guard
function isSafeDest(dest, workspace) {
try {
const real = fs.realpathSync(path.resolve(dest));
return real.startsWith(path.resolve(workspace) + path.sep) && fs.statSync(real).isDirectory();
} catch { return false; }
} Try / catch
try {
await callMcpTool('unzip', { path: zipPath, dest });
} catch (e) {
if (e.message.startsWith('archive entry escapes destination')) {
// recreate the destination as a plain directory and re-list the archive for bad entries
}
} Prevention
- Use a freshly created, non-symlink destination directory for extraction
- Pre-scan entry names and reject anything with .. or absolute paths
- Never reuse symlinked directories as extraction targets
When it happens
Trigger: extractGuardedArchive producing a member whose path (destAbs + name) lands outside destAbs — e.g. a name that survived earlier checks via unusual components, a destination path that itself changed, or platform quirks where the joined path normalizes outside the destination.
Common situations: Archives with names that pass the per-entry IsLocal check but combine with an unexpected destination (e.g. destination containing symlinked parents); crafted names exploiting separator normalization differences; calling unzip with a destination argument computed dynamically.
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
- invalid archive entry
- 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/ae24628990dbc620.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/mcp/tools/unzip.go:147
}
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
}
// authorizeArchiveEntry 校验归档成员的最终输出路径:必须位于目标目录内(含符号链接解析后),
// 且通过最终路径授权(工作区包含、加密笔记本、symlink 逃逸、敏感文件黑名单)。
func authorizeArchiveEntry(destAbs, entryAbs, display string) error {
rel, err := filepath.Rel(destAbs, entryAbs)
if err != nil || !filepath.IsLocal(rel) {
return fmt.Errorf("archive entry escapes destination [%s]", display)
}
resolved := util.ResolveLongestExistingParent(entryAbs)
resolvedDest := util.ResolveLongestExistingParent(destAbs)
if rel, err = filepath.Rel(resolvedDest, resolved); err != nil || !filepath.IsLocal(rel) {
return fmt.Errorf("archive entry resolves outside destination [%s]", display)
}
return authorizePath(entryAbs, display)
}
func extractArchiveEntry(entry *zip.File, destination string) error {
if entry.FileInfo().IsDir() {
return os.MkdirAll(destination, 0755)
}
if err := os.MkdirAll(filepath.Dir(destination), 0755); err != nil {
return err
}
source, err := entry.Open()
if err != nil {View on GitHub (pinned to 9f775e8a12)