{"record":{"id":"ee2ed112b303d1dd","repo":"siyuan-note/siyuan","slug":"path-escapes-workspace-s-ee2ed1","errorCode":null,"errorMessage":"path escapes workspace: %s","messagePattern":"path escapes workspace: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/mcp/tools/file.go","lineNumber":92,"sourceCode":"\t\treturn fileCopy(args)\n\tcase \"grep\":\n\t\treturn fileGrep(args)\n\tcase \"find\":\n\t\treturn fileFind(args)\n\tcase \"stat\":\n\t\treturn fileStat(args)\n\t}\n\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。","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/mcp/tools/file.go#L74-L110","documentation":"`resolvePath` joined the requested relative path onto `util.WorkspaceDir` and the result is no longer inside the workspace (failed `gulu.File.IsSubPath`). This is the primary directory-traversal guard for the MCP file tools: any path that resolves outside the workspace root is rejected.","triggerScenarios":"Calling a file tool with a `path` containing `..` segments that climb above the workspace root, an absolute path to an external location, or any value that `filepath.Join` resolves outside `WorkspaceDir`.","commonSituations":"Passing `../../etc/passwd` or `/etc/passwd`. Using an absolute path (`/home/user/secret`) instead of a workspace-relative one. A leading `/` that `filepath.Join` treats as absolute and discards the base for.","solutions":["Pass paths relative to the workspace root and free of `..` segments that escape it.","Avoid absolute paths unless they resolve inside the workspace.","If the file legitimately lives outside the workspace, move/symlink it into the workspace first (the symlink itself is then re-checked)."],"exampleFix":"// before\n{\"path\": \"../../../etc/passwd\"}\n// after\n{\"path\": \"data/notebooks/20240101/notes.md\"}","handlingStrategy":"validation","validationCode":"// Reject paths that escape the workspace before calling the file tool.\nfunc safeWorkspacePath(rel string) (string, error) {\n    abs := filepath.Join(util.WorkspaceDir, filepath.Clean(strings.ReplaceAll(rel, \"/\", string(os.PathSeparator))))\n    if !gulu.File.IsSubPath(util.WorkspaceDir, abs) {\n        return \"\", fmt.Errorf(\"path escapes workspace: %s\", rel)\n    }\n    return abs, nil\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Pass workspace-relative paths without escaping `..` segments.","Avoid absolute paths unless they resolve inside the workspace.","Validate paths on the caller side before invoking the file tool."],"tags":["mcp","file","security","path-traversal"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}