siyuan-note/siyuan · error

symlink escapes workspace: %s

Error message

symlink escapes workspace: %s

What it means

After joining the requested path inside the workspace, `util.ResolveLongestExistingParent` resolved a symlink and the resolved target itself is outside the workspace. This closes the symlink-escape loophole that would otherwise bypass the basic subpath check.

Source

Thrown at kernel/mcp/tools/file.go:100

	return CallToolResult{
		Content: []ContentItem{{Type: "text", Text: "unknown action '" + action + "', expected one of: [list, read, write, delete, rename, copy, grep, find, stat]"}},
		IsError: true,
	}, nil
}

func resolvePath(rel string) (string, error) {
	rel = filepath.Clean(strings.ReplaceAll(rel, "/", string(os.PathSeparator)))
	abs := filepath.Join(util.WorkspaceDir, rel)
	if !gulu.File.IsSubPath(util.WorkspaceDir, abs) {
		return "", fmt.Errorf("path escapes workspace: %s", rel)
	}
	// 拒绝加密笔记本目录:MCP 文件工具不能读写加密 box 下的文件(防止密文泄漏或明文破坏加密格式)
	if boxID, encrypted := rejectEncryptedPath(abs); encrypted {
		return "", fmt.Errorf("path belongs to encrypted notebook [%s]: %s", boxID, rel)
	}
	// 防止 symlink 逃逸工作区:解析符号链接后再次检查
	if resolved := util.ResolveLongestExistingParent(abs); resolved != abs && !gulu.File.IsSubPath(util.WorkspaceDir, resolved) {
		return "", fmt.Errorf("symlink escapes workspace: %s", rel)
	}
	// 禁止访问敏感文件(conf/conf.json、data/snippets/conf.json、data/templates、data/.siyuan/publishAccess.json),
	// 与 HTTP 文件 API 共用同一黑名单(见 kernel/util/path_guard.go 的 IsForbiddenAbsPath)
	if util.IsForbiddenAbsPath(abs) {
		return "", fmt.Errorf("access to sensitive workspace file is forbidden: %s", rel)
	}
	return abs, nil
}

// rejectEncryptedPath 检查路径是否属于加密笔记本(含 symlink 绕过),返回 boxID 和是否为加密 box。
func rejectEncryptedPath(absPath string) (boxID string, encrypted bool) {
	boxID = model.EncryptedRawPathBoxID(absPath)
	return boxID, boxID != ""
}

func fileList(args map[string]any) (CallToolResult, error) {
	p, _ := args["path"].(string)
	if p == "" {

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Remove or repoint the offending symlink so its target stays inside the workspace.
  2. Copy the real files into the workspace instead of symlinking to an external location.
  3. If external access is genuinely required, use a non-MCP path; the file tool intentionally forbids it.
Defensive patterns

Strategy: validation

Validate before calling

// Resolve symlinks and re-check the workspace boundary before invoking the tool.
resolved := util.ResolveLongestExistingParent(abs)
if resolved != abs && !gulu.File.IsSubPath(util.WorkspaceDir, resolved) {
    return "", fmt.Errorf("symlink escapes workspace")
}

Prevention

When it happens

Trigger: A file or directory inside the workspace is a symlink whose target points outside `WorkspaceDir`; the file tool is asked to operate on that symlinked path.

Common situations: A user (or another tool) created a symlink like `data/notes -> /etc` inside the workspace to access external files. A legitimately external assets folder was symlinked in for convenience.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/6c2838c41a711fdd. Report an issue: GitHub.