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

  1. Verify the destination directory passed to the unzip tool is a plain, real (non-symlink) directory inside the workspace
  2. Re-list the archive entries and remove any with .., absolute, or otherwise anomalous names, then repack
  3. 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

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


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)