modelcontextprotocol/servers · error · Error

Access denied - symlink target outside allowed directories:

Error message

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

What it means

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.

Source

Thrown at src/filesystem/lib.ts:133

    const exactMatch = entries.find(entry => entry === requestedPart);
    const equivalentMatches = exactMatch
      ? [exactMatch]
      : entries.filter(entry => entry.normalize('NFC') === requestedPart.normalize('NFC'));

    if (equivalentMatches.length > 1) {
      throw new Error(`Ambiguous Unicode path component: ${requestedPart}`);
    }

    if (equivalentMatches.length === 0) {
      // Nothing below this point exists yet, so there are no symlinks left to
      // resolve. currentPath is already realpath'd and inside an allowed
      // directory; append the missing tail so create_directory can mkdir -p it.
      return path.join(currentPath, ...relativeParts.slice(index));
    }

    currentPath = await fs.realpath(path.join(currentPath, equivalentMatches[0]));
    if (!isPathWithinAllowedDirectories(normalizePath(currentPath), allowedDirectories)) {
      throw new Error(`Access denied - symlink target outside allowed directories: ${currentPath} not in ${allowedDirectories.join(', ')}`);
    }
  }

  return currentPath;
}

export async function validatePath(requestedPath: string): Promise<string> {
  const expandedPath = expandHome(requestedPath);
  // Do not silently reinterpret a Windows drive path as a relative POSIX path.
  // This would create a literal filename such as `C:\\Users\\...` inside the
  // allowed root and report success for the wrong location.
  if (process.platform !== 'win32' && /^(?:[A-Za-z]:)(?:[\\/]|$)/.test(expandedPath)) {
    throw new Error(`Access denied - Windows-style path received on a POSIX host: ${requestedPath}`);
  }
  const absolute = path.isAbsolute(expandedPath)
    ? path.resolve(expandedPath)
    : resolveRelativePathAgainstAllowedDirectories(expandedPath);

View on GitHub (pinned to 579c3903f3)

Solutions

  1. Remove or retarget the symlink so its realpath stays inside the allowed directories
  2. Add the symlink's real target directory to the server's allowedDirectories argument (if policy permits) and restart
  3. Replace the symlink with a bind mount or copy of the content inside the allowed root

Example fix

# before
ln -s /etc/passwd /allowed/data/passwd-link
read_file('/allowed/data/passwd-link')  # Access denied
# after: keep target inside sandbox
cp /etc/passwd /allowed/data/passwd.txt
read_file('/allowed/data/passwd.txt')
Defensive patterns

Strategy: validation

Validate before calling

const real = await fs.realpath(p).catch(() => null);
const allowed = [ /* configured allowedDirectories */ ];
if (real && !allowed.some(dir => real === dir || real.startsWith(dir + path.sep))) {
  throw new Error(`symlink target outside sandbox: ${real}`);
}

Type guard

function isWithinAllowed(realPath: string, allowedDirs: string[]): boolean {
  return allowedDirs.some(dir => realPath === dir || realPath.startsWith(dir + path.sep));
}

Prevention

When it happens

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

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

Understand the failure class

Related errors


AI-assisted analysis of modelcontextprotocol/servers@579c3903f3 (2026-09-01). Data as JSON: /api/errors/f64bd0ccfd9964e0. Report an issue: GitHub.