{"record":{"id":"58bf0f474f28d636","repo":"CherryHQ/cherry-studio","slug":"access-denied-path-is-outside-the-configured-work","errorCode":null,"errorMessage":"Access denied: Path is outside the configured workspace root: ${requestedPath}","messagePattern":"Access denied: Path is outside the configured workspace root: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/mcp/servers/filesystem/types.ts","lineNumber":94,"sourceCode":"  if (normalizedTargetPath === normalizedRootPath) {\n    return true\n  }\n\n  const relativePath = path.relative(normalizedRootPath, normalizedTargetPath)\n  return relativePath !== '' && !relativePath.startsWith('..') && !path.isAbsolute(relativePath)\n}\n\n// Security validation\nexport async function validatePath(requestedPath: string, baseDir?: string): Promise<string> {\n  const expandedPath = expandHome(requestedPath)\n  const root = expandHome(baseDir ?? process.cwd())\n  const absolute = path.isAbsolute(expandedPath) ? path.resolve(expandedPath) : path.resolve(root, expandedPath)\n\n  const resolvedRoot = await resolveRealOrNearestExistingPath(path.resolve(root))\n  const resolvedPath = await resolveRealOrNearestExistingPath(absolute)\n\n  if (!isPathWithinRoot(resolvedPath, resolvedRoot)) {\n    throw new Error(`Access denied: Path is outside the configured workspace root: ${requestedPath}`)\n  }\n\n  return resolvedPath\n}\n\n// ============================================================================\n// Edit Tool Utilities - Fuzzy matching replacers from opencode\n// ============================================================================\n\nexport type Replacer = (content: string, find: string) => Generator<string, void, unknown>\n\n// Similarity thresholds for block anchor fallback matching\nconst SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0.0\nconst MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.3\n\n/**\n * Levenshtein distance algorithm implementation\n */","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/mcp/servers/filesystem/types.ts#L76-L112","documentation":"Thrown by validatePath (types.ts:85) when, after expanding ~, resolving to absolute, and running realpath/nearest-existing-ancestor resolution on both the root and the target, isPathWithinRoot returns false. This is the security boundary of the filesystem MCP server: it prevents path traversal (../), symlink escapes, and absolute-path hijacks from reaching read/write/edit/grep tools. The original requestedPath (pre-resolution) is included in the message.","triggerScenarios":"Passing an absolute path outside baseDir; using ../ to climb above the workspace root; a symlink inside the workspace whose realpath target is outside the root; on Windows, a different-drive path (C:\\ vs D:\\) whose relative path becomes absolute; baseDir undefined causing resolution against process.cwd() that the caller did not intend.","commonSituations":"Caller builds a path from user input without confining it; symlink in node_modules pointing outside the project; baseDir not passed (defaults to cwd) when the server is launched from an unexpected working directory; home-dir expansion (~) resolving outside the configured root.","solutions":["Ensure file_path resolves under the same baseDir passed to the tool; strip ../ sequences or reject them upstream.","Pass the correct baseDir explicitly rather than relying on process.cwd().","Resolve symlinks on the caller side and confirm the realpath is still within the workspace root.","On Windows, keep paths on the same drive as baseDir."],"exampleFix":"// before\nawait handleReadTool({ file_path: '/etc/passwd' }, '/workspace') // throws: Access denied\nawait handleReadTool({ file_path: '../../../secret' }, '/workspace') // throws: Access denied\n\n// after\nawait handleReadTool({ file_path: 'src/config.json' }, '/workspace')","handlingStrategy":"validation","validationCode":"import { validatePath } from '../types'\n// Pre-flight: throws the same error before the tool does any work\nconst resolved = await validatePath(requestedPath, baseDir)\n// or, lightweight local check:\nfunction withinRoot(p: string, root: string): boolean {\n  const rel = path.relative(path.resolve(root), path.resolve(root, p))\n  return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))\n}","typeGuard":null,"tryCatchPattern":"try {\n  await handleReadTool(args, baseDir)\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Access denied')) {\n    // reject the user input; do not retry with a different escape attempt\n  } else throw e\n}","preventionTips":["Always pass an explicit baseDir; do not rely on process.cwd().","Reject or sanitize ../ and absolute paths in user input before they reach the tool.","Resolve symlinks upstream and re-check the realpath is within root.","On Windows, keep paths on the same drive as baseDir."],"tags":["filesystem","security","validation","mcp","path-traversal","symlink"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}