{"record":{"id":"6c2838c41a711fdd","repo":"siyuan-note/siyuan","slug":"symlink-escapes-workspace-s","errorCode":null,"errorMessage":"symlink escapes workspace: %s","messagePattern":"symlink escapes workspace: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/mcp/tools/file.go","lineNumber":100,"sourceCode":"\treturn CallToolResult{\n\t\tContent: []ContentItem{{Type: \"text\", Text: \"unknown action '\" + action + \"', expected one of: [list, read, write, delete, rename, copy, grep, find, stat]\"}},\n\t\tIsError: true,\n\t}, nil\n}\n\nfunc resolvePath(rel string) (string, error) {\n\trel = filepath.Clean(strings.ReplaceAll(rel, \"/\", string(os.PathSeparator)))\n\tabs := filepath.Join(util.WorkspaceDir, rel)\n\tif !gulu.File.IsSubPath(util.WorkspaceDir, abs) {\n\t\treturn \"\", fmt.Errorf(\"path escapes workspace: %s\", rel)\n\t}\n\t// 拒绝加密笔记本目录：MCP 文件工具不能读写加密 box 下的文件（防止密文泄漏或明文破坏加密格式）\n\tif boxID, encrypted := rejectEncryptedPath(abs); encrypted {\n\t\treturn \"\", fmt.Errorf(\"path belongs to encrypted notebook [%s]: %s\", boxID, rel)\n\t}\n\t// 防止 symlink 逃逸工作区：解析符号链接后再次检查\n\tif resolved := util.ResolveLongestExistingParent(abs); resolved != abs && !gulu.File.IsSubPath(util.WorkspaceDir, resolved) {\n\t\treturn \"\", fmt.Errorf(\"symlink escapes workspace: %s\", rel)\n\t}\n\t// 禁止访问敏感文件（conf/conf.json、data/snippets/conf.json、data/templates、data/.siyuan/publishAccess.json），\n\t// 与 HTTP 文件 API 共用同一黑名单（见 kernel/util/path_guard.go 的 IsForbiddenAbsPath）\n\tif util.IsForbiddenAbsPath(abs) {\n\t\treturn \"\", fmt.Errorf(\"access to sensitive workspace file is forbidden: %s\", rel)\n\t}\n\treturn abs, nil\n}\n\n// rejectEncryptedPath 检查路径是否属于加密笔记本（含 symlink 绕过），返回 boxID 和是否为加密 box。\nfunc rejectEncryptedPath(absPath string) (boxID string, encrypted bool) {\n\tboxID = model.EncryptedRawPathBoxID(absPath)\n\treturn boxID, boxID != \"\"\n}\n\nfunc fileList(args map[string]any) (CallToolResult, error) {\n\tp, _ := args[\"path\"].(string)\n\tif p == \"\" {","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/mcp/tools/file.go#L82-L118","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Remove or repoint the offending symlink so its target stays inside the workspace.","Copy the real files into the workspace instead of symlinking to an external location.","If external access is genuinely required, use a non-MCP path; the file tool intentionally forbids it."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Resolve symlinks and re-check the workspace boundary before invoking the tool.\nresolved := util.ResolveLongestExistingParent(abs)\nif resolved != abs && !gulu.File.IsSubPath(util.WorkspaceDir, resolved) {\n    return \"\", fmt.Errorf(\"symlink escapes workspace\")\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Do not create symlinks inside the workspace that point outside it.","Copy external files into the workspace instead of symlinking.","Audit workspace symlinks periodically."],"tags":["mcp","file","security","symlink","path-traversal"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}