modelcontextprotocol/servers · error · Error

Access denied - symlink target outside allowed directories:

Error message

Access denied - symlink target outside allowed directories: ${realPath} not in ${allowedDirectories.join(', ')}

What it means

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.

Source

Thrown at src/filesystem/lib.ts:119

  const absolute = path.isAbsolute(expandedPath)
    ? path.resolve(expandedPath)
    : resolveRelativePathAgainstAllowedDirectories(expandedPath);

  const normalizedRequested = normalizePath(absolute);

  // Security: Check if path is within allowed directories before any file operations
  const isAllowed = isPathWithinAllowedDirectories(normalizedRequested, allowedDirectories);
  if (!isAllowed) {
    throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`);
  }

  // Security: Handle symlinks by checking their real path to prevent symlink attacks
  // This prevents attackers from creating symlinks that point outside allowed directories
  try {
    const realPath = await fs.realpath(absolute);
    const normalizedReal = normalizePath(realPath);
    if (!isPathWithinAllowedDirectories(normalizedReal, allowedDirectories)) {
      throw new Error(`Access denied - symlink target outside allowed directories: ${realPath} not in ${allowedDirectories.join(', ')}`);
    }
    return realPath;
  } catch (error) {
    // Security: For new files that don't exist yet, verify parent directory
    // This ensures we can't create files in unauthorized locations
    if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
      const parentDir = path.dirname(absolute);
      try {
        const realParentPath = await fs.realpath(parentDir);
        const normalizedParent = normalizePath(realParentPath);
        if (!isPathWithinAllowedDirectories(normalizedParent, allowedDirectories)) {
          throw new Error(`Access denied - parent directory outside allowed directories: ${realParentPath} not in ${allowedDirectories.join(', ')}`);
        }
        return absolute;
      } catch {
        throw new Error(`Parent directory does not exist: ${parentDir}`);
      }
    }

View on GitHub (pinned to 76d64c822f)

Solutions

  1. Either remove the offending symlink or point it at a target inside the allowed directories.
  2. Add the symlink's real target directory to the allowed directories (CLI args or roots), understanding the security implication.
  3. Audit symlinks under allowed dirs before granting access: `find <dir> -type l -exec readlink {} \;`.

Example fix

# before: /home/me/projects/link -> /etc/secrets (outside allowlist)
# after: re-point the link inside an allowed dir, OR add the target's parent to allowlist
ln -sfn /home/me/shared/data /home/me/projects/link
Defensive patterns

Strategy: try-catch

Validate before calling

import fs from 'node:fs/promises';
async function symlinkTargetInside(p: string, allowed: string[]): Promise<boolean> {
  try {
    const real = await fs.realpath(p);
    return allowed.some(d => real.startsWith(path.resolve(d) + path.sep) || real === path.resolve(d));
  } catch { return true; /* let the server decide on missing paths */ }
}

Try / catch

try {
  await readFile({ path });
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Access denied - symlink target')) {
    // remove/repoint the symlink or add its target's parent to allowed dirs
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of modelcontextprotocol/servers@76d64c822f (2026-08-12). Data as JSON: /api/errors/632e004ee3015652. Report an issue: GitHub.