{"record":{"id":"632e004ee3015652","repo":"modelcontextprotocol/servers","slug":"access-denied-symlink-target-outside-allowed-dir","errorCode":null,"errorMessage":"Access denied - symlink target outside allowed directories: ${realPath} not in ${allowedDirectories.join(', ')}","messagePattern":"Access denied - symlink target outside allowed directories: (.+?) not in (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/filesystem/lib.ts","lineNumber":119,"sourceCode":"  const absolute = path.isAbsolute(expandedPath)\n    ? path.resolve(expandedPath)\n    : resolveRelativePathAgainstAllowedDirectories(expandedPath);\n\n  const normalizedRequested = normalizePath(absolute);\n\n  // Security: Check if path is within allowed directories before any file operations\n  const isAllowed = isPathWithinAllowedDirectories(normalizedRequested, allowedDirectories);\n  if (!isAllowed) {\n    throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`);\n  }\n\n  // Security: Handle symlinks by checking their real path to prevent symlink attacks\n  // This prevents attackers from creating symlinks that point outside allowed directories\n  try {\n    const realPath = await fs.realpath(absolute);\n    const normalizedReal = normalizePath(realPath);\n    if (!isPathWithinAllowedDirectories(normalizedReal, allowedDirectories)) {\n      throw new Error(`Access denied - symlink target outside allowed directories: ${realPath} not in ${allowedDirectories.join(', ')}`);\n    }\n    return realPath;\n  } catch (error) {\n    // Security: For new files that don't exist yet, verify parent directory\n    // This ensures we can't create files in unauthorized locations\n    if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n      const parentDir = path.dirname(absolute);\n      try {\n        const realParentPath = await fs.realpath(parentDir);\n        const normalizedParent = normalizePath(realParentPath);\n        if (!isPathWithinAllowedDirectories(normalizedParent, allowedDirectories)) {\n          throw new Error(`Access denied - parent directory outside allowed directories: ${realParentPath} not in ${allowedDirectories.join(', ')}`);\n        }\n        return absolute;\n      } catch {\n        throw new Error(`Parent directory does not exist: ${parentDir}`);\n      }\n    }","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/modelcontextprotocol/servers/blob/76d64c822f5125032f89eb71dbdb94e42b434821/src/filesystem/lib.ts#L101-L137","documentation":"Thrown by `validatePath` after resolving the realpath of an existing path, when the symlink's real target lies outside the allowed directories. This is the anti-symlink-attack guard: even if the requested path text is inside the allowlist, a symlink within it could point elsewhere, so the server resolves `fs.realpath` and re-checks the boundary.","triggerScenarios":"A file or directory inside an allowed dir is a symlink whose target resolves outside the allowlist — e.g. `/home/me/projects/link -> /etc/secrets`. The requested path passes the first check (error 16 path) but fails the realpath check.","commonSituations":"Pre-existing symlinks in the workspace pointing outside, a compromised/attacker-created symlink inside an allowed dir, or legitimate cross-dir symlinks (e.g. to a shared lib) that the operator forgot to allowlist.","solutions":["Either remove the offending symlink or point it at a target inside the allowed directories.","Add the symlink's real target directory to the allowed directories (CLI args or roots), understanding the security implication.","Audit symlinks under allowed dirs before granting access: `find <dir> -type l -exec readlink {} \\;`."],"exampleFix":"# before: /home/me/projects/link -> /etc/secrets (outside allowlist)\n# after: re-point the link inside an allowed dir, OR add the target's parent to allowlist\nln -sfn /home/me/shared/data /home/me/projects/link","handlingStrategy":"try-catch","validationCode":"import fs from 'node:fs/promises';\nasync function symlinkTargetInside(p: string, allowed: string[]): Promise<boolean> {\n  try {\n    const real = await fs.realpath(p);\n    return allowed.some(d => real.startsWith(path.resolve(d) + path.sep) || real === path.resolve(d));\n  } catch { return true; /* let the server decide on missing paths */ }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await readFile({ path });\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Access denied - symlink target')) {\n    // remove/repoint the symlink or add its target's parent to allowed dirs\n  }\n}","preventionTips":["Audit symlinks under allowed dirs: find <dir> -type l.","Don't create symlinks that escape the sandbox.","If a cross-dir symlink is legitimate, allowlist the real target dir."],"tags":["mcp","typescript","filesystem-server","security","symlink","access-control"],"backgroundTag":null,"analyzedSha":"76d64c822f5125032f89eb71dbdb94e42b434821","analyzedAt":"2026-08-12T10:02:41.718Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}