{"record":{"id":"f64bd0ccfd9964e0","repo":"modelcontextprotocol/servers","slug":"access-denied-symlink-target-outside-allowed-dir-f64bd0","errorCode":null,"errorMessage":"Access denied - symlink target outside allowed directories: ${currentPath} not in ${allowedDirectories.join(', ')}","messagePattern":"Access denied - symlink target outside allowed directories: (.+?) not in (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/filesystem/lib.ts","lineNumber":133,"sourceCode":"    const exactMatch = entries.find(entry => entry === requestedPart);\n    const equivalentMatches = exactMatch\n      ? [exactMatch]\n      : entries.filter(entry => entry.normalize('NFC') === requestedPart.normalize('NFC'));\n\n    if (equivalentMatches.length > 1) {\n      throw new Error(`Ambiguous Unicode path component: ${requestedPart}`);\n    }\n\n    if (equivalentMatches.length === 0) {\n      // Nothing below this point exists yet, so there are no symlinks left to\n      // resolve. currentPath is already realpath'd and inside an allowed\n      // directory; append the missing tail so create_directory can mkdir -p it.\n      return path.join(currentPath, ...relativeParts.slice(index));\n    }\n\n    currentPath = await fs.realpath(path.join(currentPath, equivalentMatches[0]));\n    if (!isPathWithinAllowedDirectories(normalizePath(currentPath), allowedDirectories)) {\n      throw new Error(`Access denied - symlink target outside allowed directories: ${currentPath} not in ${allowedDirectories.join(', ')}`);\n    }\n  }\n\n  return currentPath;\n}\n\nexport async function validatePath(requestedPath: string): Promise<string> {\n  const expandedPath = expandHome(requestedPath);\n  // Do not silently reinterpret a Windows drive path as a relative POSIX path.\n  // This would create a literal filename such as `C:\\\\Users\\\\...` inside the\n  // allowed root and report success for the wrong location.\n  if (process.platform !== 'win32' && /^(?:[A-Za-z]:)(?:[\\\\/]|$)/.test(expandedPath)) {\n    throw new Error(`Access denied - Windows-style path received on a POSIX host: ${requestedPath}`);\n  }\n  const absolute = path.isAbsolute(expandedPath)\n    ? path.resolve(expandedPath)\n    : resolveRelativePathAgainstAllowedDirectories(expandedPath);\n","sourceCodeStart":115,"sourceCodeEnd":151,"githubUrl":"https://github.com/modelcontextprotocol/servers/blob/579c3903f30044eb702a599a74b3ae77588e722e/src/filesystem/lib.ts#L115-L151","documentation":"While resolving each path component, the library calls fs.realpath on the matched entry and re-checks that the resolved target stays inside the allowed directories. If a symlink points outside the sandbox, resolution is aborted with this error to block symlink-escape attacks. This is an intentional security guard, not a bug.","triggerScenarios":"Accessing any path (via validatePath) where a component in the chain is a symlink whose realpath resolves outside the directories configured at server startup — even if the requested path textually lies inside an allowed root.","commonSituations":"Users pointing tools at convenience links like ~/shared -> /etc or /home/user/data -> /mnt/external that were created before the server was restricted to allowedDirectories; Docker/CI mounts where allowed dirs differ from the link target.","solutions":["Remove or retarget the symlink so its realpath stays inside the allowed directories","Add the symlink's real target directory to the server's allowedDirectories argument (if policy permits) and restart","Replace the symlink with a bind mount or copy of the content inside the allowed root"],"exampleFix":"# before\nln -s /etc/passwd /allowed/data/passwd-link\nread_file('/allowed/data/passwd-link')  # Access denied\n# after: keep target inside sandbox\ncp /etc/passwd /allowed/data/passwd.txt\nread_file('/allowed/data/passwd.txt')","handlingStrategy":"validation","validationCode":"const real = await fs.realpath(p).catch(() => null);\nconst allowed = [ /* configured allowedDirectories */ ];\nif (real && !allowed.some(dir => real === dir || real.startsWith(dir + path.sep))) {\n  throw new Error(`symlink target outside sandbox: ${real}`);\n}","typeGuard":"function isWithinAllowed(realPath: string, allowedDirs: string[]): boolean {\n  return allowedDirs.some(dir => realPath === dir || realPath.startsWith(dir + path.sep));\n}","tryCatchPattern":null,"preventionTips":["Audit symlinks under allowed directories periodically (find -type l -exec realpath {})","Avoid creating convenience symlinks to system or external paths inside sandboxed roots","Pass a minimal, explicit allowedDirectories list to the server","Prefer bind mounts or copies over symlinks when sharing content into the sandbox"],"tags":["filesystem","symlink","security","access-denied"],"backgroundTag":"symlink-escape-denied","analyzedSha":"579c3903f30044eb702a599a74b3ae77588e722e","analyzedAt":"2026-09-01T06:06:22.520Z","contentChangedAt":"2026-09-01T06:06:22.520Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}