How the MCP Filesystem Server Handles Symlinks on macOS

The MCP Filesystem server prevents symlink attacks by resolving all paths to their real locations using fs.realpath and validating them against both original and dereferenced allowed directories, ensuring transparent support for macOS paths like /tmp while blocking unauthorized access.

The MCP Filesystem server in the modelcontextprotocol/servers repository implements a defense-in-depth strategy for symbolic link handling on macOS. By normalizing paths and resolving symlinks before any file operation, the server protects against directory traversal attacks while accommodating platform-specific filesystem conventions such as /tmp being a symlink to /private/tmp.

Allowed Directory Preparation with Dual Path Storage

When the server initializes, it processes each user-provided directory to establish the security boundary.

Storing Original and Resolved Paths

In src/filesystem/index.ts, lines 41-58, the server expands each allowed directory and resolves any symlinks once during startup. It stores both the original path and its resolved target in the allowed directories list. This dual storage ensures that requests using either the symbolic path (e.g., /tmp) or its real location (e.g., /private/tmp) correctly match the security policy.

This approach specifically addresses macOS behavior where system directories are symlinks. By caching both representations, the server avoids repeated filesystem calls while maintaining strict validation capabilities.

Per-Request Validation with Real Path Resolution

Every file operation triggers a validation routine that dereferences symlinks in real-time to prevent escape attacks.

The validatePath Function

Located in src/filesystem/lib.ts, lines 113-121, the validatePath function executes the following security checks:

  1. Expands tilde (~) notation and converts the path to absolute form.
  2. Calls fs.realpath to obtain the actual filesystem location, following all symlinks.
  3. Verifies that the resolved path falls within any of the previously stored allowed directories (matching against either the original or resolved variants).
  4. Rejects the request with an "Access denied – symlink target outside allowed directories" error if the real path points outside the authorized boundary.

This ensures that even if a user provides a path like /tmp/symlink-to-etc-passwd, the server resolves the true target and blocks access if it lies outside the allowed directory tree.

Root Directory Validation

When allowed directories are supplied via the MCP Roots protocol, the server applies additional symlink scrutiny.

Securing MCP Roots

In src/filesystem/roots-utils.ts, lines 45-48, the getValidRootDirectories function dereferences root paths using fs.realpath during the validation phase. This prevents a malicious or misconfigured root directory that is itself a symlink to an unauthorized location (such as ~/allowed-dir pointing to /etc) from bypassing security controls.

By resolving roots before adding them to the allowed set, the server ensures that the security boundary remains intact regardless of how the directories are accessed.

Practical Implementation Example

The following example demonstrates starting the server with a directory that may be a symlink and performing validated operations:

// Start the server with /tmp, which on macOS is a symlink to /private/tmp
$ mcp-server-filesystem /tmp

// The server internally stores both "/tmp" and "/private/tmp" as allowed entries

// Request using the symbolic path
await server.callTool("read_text_file", {
  path: "/tmp/example.txt"
});

// Request using the resolved path
await server.callTool("read_text_file", {
  path: "/private/tmp/example.txt"
});

// Both requests pass validation because validatePath:
// 1. Resolves the input to /private/tmp/example.txt via fs.realpath
// 2. Confirms it matches an allowed directory (either /tmp or /private/tmp)
// 3. Allows the operation to proceed

If a client attempts to access a file through a symlink that escapes the allowed boundary, the validation chain halts the operation before any filesystem access occurs.

Summary

  • Dual path storage in src/filesystem/index.ts (lines 41-58) caches both original and resolved allowed directories to handle macOS symlinks transparently.
  • Real-time resolution via validatePath in src/filesystem/lib.ts (lines 113-121) uses fs.realpath to dereference symlinks on every request and validate the true path location.
  • Root validation in src/filesystem/roots-utils.ts (lines 45-48) prevents symlink-based root directory attacks by resolving paths before adding them to the allowed set.
  • Security boundary enforcement returns an explicit "Access denied" error when symlink targets fall outside authorized directories.

Frequently Asked Questions

The server prevents symlink attacks by calling fs.realpath in the validatePath function to resolve all symlinks to their actual locations before performing any file operation. It then checks that the resolved path falls within the allowed directory boundaries, rejecting requests with an "Access denied – symlink target outside allowed directories" error if the target lies outside the authorized tree.

Why does the server store both the original and resolved paths for allowed directories?

The server stores both variants to accommodate macOS filesystem conventions where directories like /tmp are symlinks to /private/tmp. By caching both the symbolic and real paths during startup (in src/filesystem/index.ts, lines 41-58), the server allows clients to use either representation while maintaining a single consistent security check during request validation.

If fs.realpath resolves a requested path to a location outside the allowed directory set, the validatePath function immediately rejects the request. This check occurs before any file content is read or written, ensuring that symbolic links cannot be used to traverse into unauthorized areas of the filesystem such as /etc or /home/other-user.

When processing directories supplied via the MCP Roots protocol, the getValidRootDirectories function in src/filesystem/roots-utils.ts (lines 45-48) resolves each root using fs.realpath before adding it to the allowed list. This prevents a root directory that is a symlink to an unauthorized location from being accepted as a valid security boundary.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →