Symlink Traversal Prevention in Desktop Commander: How the MCP Server Blocks Directory Escape Attacks

Desktop Commander prevents symlink traversal attacks by resolving every symbolic link in a requested path—whether the final file exists or not—and validating the canonicalized result against a whitelist of allowed directories before executing any file operation.

Desktop Commander is a Model Context Protocol (MCP) server that exposes file-system operations to AI agents, making robust symlink traversal prevention essential for maintaining sandbox security. The server confines all read, write, and directory operations to trusted paths by canonicalizing inputs through the validatePath function in src/tools/filesystem.ts. This ensures that malicious symbolic links pointing to sensitive system locations—such as a symlink to /etc/passwd placed inside an allowed folder—are detected and rejected before any data is accessed or modified.

How the Security Mechanism Works

The core defense resides in the validatePath function (lines 44–106 of src/tools/filesystem.ts). Every public file-system API—including readFile, writeFile, and createDirectory—invokes this validator before interacting with the underlying operating system.

The validation process begins by expanding the tilde (~) character and converting relative paths to absolute form using expandHome (lines 38–42) and normalizePath (lines 34–36). The critical security logic then distinguishes between existing and non-existing targets:

  • For existing paths, the code calls fs.realpath, which automatically follows all symbolic links in the path chain and returns the true canonical location.
  • For new paths, the implementation performs a "SECURITY FIX" (lines 58–86): it resolves the nearest existing ancestor directory using fs.realpath, then re-attaches the non-existent leaf components. This guarantees that symlinks anywhere in the parent chain are revealed even when the final file does not yet exist.

Whitelist Verification with isPathAllowed

After resolving the canonical path, validatePath delegates to isPathAllowed (lines 71–115) to enforce boundary checks. This function normalizes both the resolved path and each entry in the allowedDirectories configuration (retrieved via getAllowedDirs at lines 11–25), then verifies:

  • Exact equality with an allowed directory
  • Sub-directory containment (ensuring /home/user does not erroneously match /home/username)
  • Windows drive-letter normalization for cross-platform compatibility

If the resolved path falls outside the permitted set, the function throws a validation error and emits a server_path_validation_error telemetry event via the capture utility for security monitoring.

Race Condition Mitigation

Because symlink resolution occurs before any fs.stat, fs.readFile, or fs.mkdir call, the server eliminates time-of-check to time-of-use (TOCTOU) vulnerabilities. An attacker cannot swap a legitimate file for a malicious symlink after validation but before execution, as the canonical path is computed once and passed immutably to all downstream operations.

Source Code Implementation Details

Component Purpose Location
validatePath Central validation function that resolves symlinks and enforces directory constraints. src/tools/filesystem.ts, L44–106
isPathAllowed Whitelist comparison logic with platform-specific normalization. src/tools/filesystem.ts, L71–115
getAllowedDirs Retrieves permitted directories from configManager.getConfig() or defaults to the home directory. src/tools/filesystem.ts, L11–25
normalizePath Converts paths to absolute form and handles case-insensitive normalization. src/tools/filesystem.ts, L34–36
expandHome Expands the ~ alias to the user's home directory. src/tools/filesystem.ts, L38–42

Practical Usage Examples

Reading a File Safely

When calling readFile, the symlink traversal prevention executes automatically behind the scenes:

import { readFile } from './tools/filesystem.js';

(async () => {
  try {
    const result = await readFile('~/projects/notes.txt');
    console.log('Content:', result.content);
  } catch (e) {
    console.error('Access denied:', e.message);
  }
})();

Even if ~/projects contains a malicious symlink to /etc, the validatePath call inside readFileFromDisk resolves the true location and rejects the operation if it escapes the allowed boundaries.

Writing to a New File with Parent Directory Validation

Creating new files triggers the parent-directory resolution security fix:

import { writeFile } from './tools/filesystem.js';

// Attempting to write through a symlink attack
await writeFile('~/projects/malicious-link/target.txt', 'malicious content', 'rewrite');

If ~/projects/malicious-link is a symlink pointing to /etc/passwd, the validator resolves the parent directory (~/projects) via fs.realpath, discovers the symlink destination (/etc/passwd), and rejects the operation because /etc is not in the allowedDirectories whitelist.

Creating Nested Directories Safely

The protection extends to directory creation operations:

import { createDirectory } from './tools/filesystem.js';

await createDirectory('~/projects/subdir/nested/deep');

Although subdir/nested/deep does not exist, validatePath walks up the tree to the nearest existing ancestor, resolves any symlinks encountered during the ascent, and only proceeds with mkdir if the resolved canonical path remains within the trusted boundary.

Moving Files with Dual Validation

Both source and destination paths undergo independent validation:

import { moveFile } from './tools/filesystem.js';

await moveFile('~/projects/temp.txt', '~/archive/final.txt');

This ensures that neither the source nor the destination can exploit symlink traversal to read from or write to restricted system locations.

Summary

  • Canonical resolution first: Desktop Commander always resolves symlinks using fs.realpath before checking permissions, preventing attackers from hiding malicious destinations behind relative paths or symbolic links.
  • New file protection: The "SECURITY FIX" logic (lines 58–86) specifically handles non-existent paths by resolving parent directories, closing the gap where creating new files might otherwise bypass checks.
  • Configurable boundaries: The allowedDirectories whitelist in config-manager.ts lets administrators define strict sandboxes, with all paths normalized for case-insensitive and cross-platform consistency.
  • Atomic validation: By computing the canonical path once and reusing it for all subsequent operations, the server eliminates race conditions between validation and execution.
  • Telemetry integration: Failed validation attempts trigger server_path_validation_error events, enabling security monitoring and audit trails for suspicious access patterns.

Frequently Asked Questions

A symlink traversal attack occurs when an attacker places a symbolic link inside an allowed directory that points to a sensitive location outside the sandbox, such as /etc/passwd or C:\Windows\System32. When the application follows the link, it inadvertently accesses files outside its intended scope. For MCP servers like Desktop Commander that grant AI agents file-system access, this could allow unauthorized reading of credentials or modification of system files. The validatePath function prevents this by resolving all symlinks and verifying the final canonical location against a whitelist before any file operation executes.

When the target file does not exist, the server cannot use fs.realpath on the full path. Instead, it implements a security fix (lines 58–86 in src/tools/filesystem.ts) that finds the nearest existing ancestor directory, resolves that ancestor using fs.realpath to reveal any symlinks in the parent chain, then re-attaches the non-existent leaf components. This ensures that even if an attacker creates a symlink to a restricted directory and attempts to write a new file through it, the resolved canonical path is checked against allowedDirectories and rejected if it escapes the sandbox.

What happens when a path fails validation?

If isPathAllowed determines that a resolved path falls outside the configured allowedDirectories, validatePath throws an error with the message "Path is outside allowed directories". Simultaneously, the server emits a server_path_validation_error telemetry event via the capture utility in src/utils/capture.ts. The operation is aborted before any actual file system call (read, write, or directory creation) occurs, ensuring no data leakage or unauthorized modification takes place.

Can I customize which directories Desktop Commander is allowed to access?

Yes, the whitelist is fully configurable through the config-manager.ts module. The getAllowedDirs function retrieves the allowedDirectories array from the server configuration; if none are specified, it defaults to the user's home directory. Administrators can define multiple allowed roots, and the isPathAllowed function will permit operations within any of those directories or their subdirectories, provided the resolved canonical path matches exactly or begins with an allowed directory path followed by a path separator.

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 →