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.
Canonicalization and Symlink Resolution
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/userdoes 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.realpathbefore 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
allowedDirectorieswhitelist inconfig-manager.tslets 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_errorevents, enabling security monitoring and audit trails for suspicious access patterns.
Frequently Asked Questions
What is a symlink traversal attack and why does it matter for MCP servers?
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.
How does Desktop Commander handle symlinks when creating new files that don't exist yet?
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →