# How to Implement Path Validation in the MCP Filesystem Server

> Implement path validation in the MCP Filesystem server using validatePath() and a whitelist architecture to securely normalize paths and resolve symlinks before filesystem access.

- Repository: [Model Context Protocol/servers](https://github.com/modelcontextprotocol/servers)
- Tags: how-to-guide
- Published: 2026-03-01

---

**The MCP Filesystem server implements path validation through a whitelist-based architecture using `validatePath()` in [`src/filesystem/lib.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/lib.ts), which normalizes paths, checks against allowed directories, and resolves symlinks before any file system access occurs.**

The `modelcontextprotocol/servers` repository provides a secure filesystem server that prevents directory traversal and symlink attacks through rigorous path validation. Implementing path validation in the MCP Filesystem server requires understanding three core modules that work together to create a fail-closed security model.

## Core Architecture of Path Validation

The validation system operates through a defense-in-depth strategy spread across three source files: [`src/filesystem/path-validation.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/path-validation.ts) for core logic, [`src/filesystem/path-utils.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/path-utils.ts) for normalization, and [`src/filesystem/lib.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/lib.ts) for the public API.

### The Whitelist System (`setAllowedDirectories`)

The server maintains a global whitelist of permitted directories that is set once at startup and enforced on every request.

```typescript
let allowedDirectories: string[] = [];

export function setAllowedDirectories(dirs: string[]): void {
  allowedDirectories = [...dirs];
}

export function getAllowedDirectories(): string[] {
  return [...allowedDirectories];
}

```

In [`src/filesystem/__main__.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/__main__.ts), you initialize the server by calling `setAllowedDirectories()` with your allowed roots. This creates a sandbox that the server cannot escape, even if symbolic links or relative path tricks are attempted.

### Path Normalization ([`path-utils.ts`](https://github.com/modelcontextprotocol/servers/blob/main/path-utils.ts))

Before any validation occurs, paths must be converted to a canonical form. The `normalizePath()` function in [`src/filesystem/path-utils.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/path-utils.ts) handles OS-specific edge cases:

- Strips surrounding quotes and whitespace
- Preserves WSL paths (`/mnt/…`) and normal Unix paths
- Converts Unix-style Windows drives (`/c/`) only on Windows
- Collapses duplicate separators and resolves `.`/`..` components
- Forces backslashes and capitalizes drive letters on Windows

This normalization ensures that **mixed separators or relative components** cannot bypass the whitelist through string manipulation.

### The Validation Engine ([`path-validation.ts`](https://github.com/modelcontextprotocol/servers/blob/main/path-validation.ts))

The core security logic lives in `isPathWithinAllowedDirectories()` inside [`src/filesystem/path-validation.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/path-validation.ts):

```typescript
export function isPathWithinAllowedDirectories(
  absolutePath: string,
  allowedDirectories: string[]
): boolean {
  // Type & null-byte safety checks
  // Normalize the candidate path with path.resolve
  // Verify the result is absolute
  // For each whitelist entry:
  //   - Normalize the directory
  //   - Ensure it is absolute
  //   - Return true if candidate equals directory,
  //     or starts with "<directory><sep>" (handling root and Windows-drive-root cases)
}

```

This function **rejects null bytes** to stop injection attacks, throws on relative input after normalization, and handles Windows special cases including root paths (`C:\`) and UNC paths.

## The `validatePath()` Implementation

The high-level API in [`src/filesystem/lib.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/lib.ts) orchestrates the complete validation workflow through `validatePath()`:

```typescript
export async function validatePath(requestedPath: string): Promise<string> {
  const expanded = expandHome(requestedPath);               // "~" → $HOME
  const absolute = path.isAbsolute(expanded)
    ? path.resolve(expanded)
    : resolveRelativePathAgainstAllowedDirectories(expanded); // uses whitelist

  const normalizedRequested = normalizePath(absolute);

  // 1️⃣ Whitelist check (fast)
  if (!isPathWithinAllowedDirectories(normalizedRequested, allowedDirectories))
    throw new Error(`Access denied …`);

  // 2️⃣ Symlink resolution
  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`);
    return realPath;
  } catch (e) {
    // 3️⃣ ENOENT handling (new file)
    if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
      const parentDir = path.dirname(absolute);
      const realParent = await fs.realpath(parentDir);
      const normalizedParent = normalizePath(realParent);
      if (!isPathWithinAllowedDirectories(normalizedParent, allowedDirectories))
        throw new Error(`Access denied – parent dir outside allowed directories`);
      return absolute; // safe to create new file here
    }
    throw e;
  }
}

```

### Security Guarantees

Each validation step protects against specific attack vectors:

- **Whitelist test**: Blocks direct traversal (`../etc/passwd`) or absolute paths pointing outside allowed roots
- **`fs.realpath` resolution**: Prevents symlink attacks where a malicious link inside an allowed directory points to sensitive system files
- **Parent-directory check on ENOENT**: Stops creation of new files in unauthorized locations (e.g., [`../outside/file.txt`](https://github.com/modelcontextprotocol/servers/blob/main/../outside/file.txt))
- **`resolveRelativePathAgainstAllowedDirectories`**: Guarantees relative requests anchor to a whitelist entry before any I/O occurs

## Practical Implementation Example

Configure the whitelist at startup, then validate every user-supplied path before operations:

```typescript
import { setAllowedDirectories, validatePath } from './lib.js';
import * as fs from 'fs/promises';

// 1️⃣ Initialize allowed directories (call once at startup)
setAllowedDirectories([
  '/srv/mcp/data',                // Linux/Unix
  'C:\\MCP\\Data'                // Windows
]);

// 2️⃣ Validate before any file operation
async function readUserFile(userPath: string) {
  try {
    const safePath = await validatePath(userPath);   // throws if unsafe
    const content = await fs.readFile(safePath, 'utf-8');
    return content;
  } catch (err) {
    // Convert to HTTP 403 or similar
    console.error(err);
    throw new Error('Forbidden: requested path is not allowed');
  }
}

// Example calls
await readUserFile('project/readme.md');          // relative → resolved against whitelist
await readUserFile('/srv/mcp/data/notes.txt');   // absolute, allowed
await readUserFile('../../etc/passwd');           // ❌ throws – outside allowed dirs

```

**Key implementation details**:

- Call `setAllowedDirectories()` **once** during server initialization
- `validatePath()` returns the **real, absolute path** after symlink resolution
- All I/O occurs **after** validation, ensuring fail-closed behavior
- Relative paths are automatically resolved against the whitelist, not the process working directory

## Summary

- Use `setAllowedDirectories()` at startup to define the sandbox roots in [`src/filesystem/lib.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/lib.ts)
- Call `await validatePath()` from [`src/filesystem/lib.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/lib.ts) before every filesystem operation to enforce the whitelist
- The system validates path membership **before** resolving symlinks, then re-validates the symlink target
- For new file creation, the parent directory is validated against the whitelist when the target does not exist
- All paths are normalized using OS-aware logic in [`src/filesystem/path-utils.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/path-utils.ts) to prevent bypasses through separator tricks

## Frequently Asked Questions

### How does the MCP Filesystem server prevent symlink attacks?

The server uses `fs.realpath()` in `validatePath()` to resolve symbolic links to their actual targets, then normalizes and validates that resolved path against the whitelist. If a symlink inside an allowed directory points outside the whitelist (e.g., to `/etc/passwd`), the second validation check throws an access denied error before any file content is read.

### Can users access files using relative paths like `../`?

No. Relative paths are resolved using `resolveRelativePathAgainstAllowedDirectories()` in [`src/filesystem/lib.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/lib.ts), which anchors them to an allowed directory before validation. Even if a user requests `../../etc/passwd`, the system attempts to resolve it within the whitelist confines. The subsequent `isPathWithinAllowedDirectories()` check in [`src/filesystem/path-validation.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/path-validation.ts) detects that the resolved absolute path falls outside the allowed roots and throws an error.

### What happens when validating a path for a new file that doesn't exist yet?

When `fs.realpath()` throws an `ENOENT` error (file not found), the server enters a special handling branch in `validatePath()`. It extracts the parent directory using `path.dirname()`, resolves and validates that parent against the whitelist, and only permits the operation if the parent lies within allowed directories. This prevents attackers from creating files in unauthorized locations while allowing legitimate new file creation inside the sandbox.

### How are Windows and Unix path formats handled differently?

The `normalizePath()` function in [`src/filesystem/path-utils.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/path-utils.ts) detects the operating system and applies platform-specific rules. On Windows, it forces backslash separators, capitalizes drive letters (ensuring `c:\` and `C:\` match), and handles UNC paths. On Unix systems, it preserves WSL mount points and uses forward slashes. This ensures path validation works correctly across platforms without false negatives due to case sensitivity or separator mismatches.