Symlink Traversal Prevention in Desktop Commander: How validatePath Secures MCP File Operations
Desktop Commander prevents symlink traversal attacks by resolving all symbolic links in a path before any filesystem operation and validating the canonical result against a whitelist of allowed directories.
Desktop Commander is an MCP (Model Context Protocol) client that exposes filesystem operations to AI assistants, making robust symlink traversal prevention essential for security. The validatePath function in src/tools/filesystem.ts serves as the primary security gate, ensuring all file operations remain confined to permitted directories even when symbolic links attempt to redirect access to sensitive system locations like /etc/passwd.
How Desktop Commander Mitigates Symlink Attacks
If an attacker places a symbolic link inside an allowed folder that points to a restricted location, an unprotected MCP client might unwittingly read or write outside its sandbox. Desktop Commander implements a three-step defense: resolve every symlink in the requested path, normalize the absolute path, and compare it against a whitelist of allowed directories before executing any operation.
The validatePath Security Gate
All public filesystem APIs—including readFile, writeFile, createDirectory, and moveFile—call validatePath as their first operation. According to the source code in src/handlers/filesystem-handlers.ts, these handlers immediately pass user input through this validation layer before touching the disk.
The function performs three decisive actions:
- Canonicalization – Expands
~and resolves relative components vianormalizePath(L 34‑36) andexpandHome(L 38‑42). - Symlink Resolution – Uses
fs.realpathfor existing files and a parent-directory walk for new paths (the "SECURITY FIX" at L 58‑86). - Whitelist Enforcement – Validates the resolved path against
allowedDirectoriesviaisPathAllowed(L 71‑115), throwing an error and emittingserver_path_validation_errortelemetry if the path escapes the sandbox.
Canonical Path Resolution
Before whitelist comparison, the path undergoes rigorous normalization. The expandHome utility converts ~ to the actual home directory, while normalizePath produces an absolute path and handles case-insensitive platforms by lowercasing paths when necessary. This ensures that /home/user/../projects and ~/projects resolve to identical canonical strings before security checks apply.
Step-by-Step Symlink Resolution Logic
The validatePath implementation distinguishes between existing file paths and new file paths, applying different resolution strategies to handle symlinks in parent directories.
Resolving Existing Files
For paths where the target already exists, the function calls fs.realpath (L 44‑48). This native Node.js method automatically follows all symbolic links in the path chain and returns the true canonical location. If the file exists, this single call reveals any redirection attempts hidden in the path.
Resolving New File Paths (The Critical Security Fix)
When fs.realpath throws ENOENT (file does not exist), the code executes the security-critical logic at L 58‑86. Instead of failing, it:
- Walks up the directory tree to find the nearest existing ancestor.
- Resolves that ancestor using
fs.realpathto reveal any symlinks in parent directories. - Re-attaches the non-existent leaf component to the resolved parent path.
This prevents a bypass where an attacker places a symlink in a parent directory of a new file. For example, if ~/projects/malicious points to /etc and the user attempts to create ~/projects/malicious/passwd, the resolution catches the symlink in the parent chain and reveals the true target path /etc/passwd.
Whitelist Validation and Enforcement
After resolution, isPathAllowed (L 71‑115) validates the canonical path against the whitelist retrieved from getAllowedDirs (L 11‑25). The whitelist checks include:
- Exact match – The resolved path exactly equals an allowed directory.
- Sub-directory validation – Ensures the resolved path is genuinely a child of an allowed root (preventing
/home/userfrom matching/home/usernamevia prefix checks). - Windows drive handling – Special logic for Windows drive letters (e.g.,
c:).
If validation fails, the function throws an error at L 92‑100 and logs a server_path_validation_error event via the telemetry helper in src/utils/capture.ts. Only after passing all checks does validatePath return the safe, resolved path (L 101‑106) for use by downstream operations.
Practical Implementation Examples
The validation happens automatically when using the public API. Here are common scenarios:
Reading a File Safely
import { readFile } from './tools/filesystem.js';
(async () => {
try {
const result = await readFile('~/projects/config.json');
console.log('Content:', result.content);
} catch (e) {
console.error('Security validation failed:', e.message);
}
})();
Behind the scenes, readFile → validatePath ensures the path resolves inside the permitted directories before fs.readFile executes.
Writing Files Without Symlink Escapes
import { writeFile } from './tools/filesystem.js';
await writeFile('~/projects/report.txt', ' confidential data', 'rewrite');
If a malicious symlink existed at ~/projects/malicious-link pointing to /etc/passwd, attempting to write to ~/projects/malicious-link would resolve the parent directory, detect the symlink, and reject the operation because /etc/passwd falls outside the allowed directories.
Creating Nested Directories
import { createDirectory } from './tools/filesystem.js';
await createDirectory('~/projects/2024/quarterly/nested');
Even though 2024/quarterly/nested does not exist, validatePath walks up the tree, resolves any symlinks in existing ancestors, and validates the final resolved location before fs.mkdir creates the folders.
Moving Files with Dual Validation
import { moveFile } from './tools/filesystem.js';
await moveFile('~/projects/temp.txt', '~/projects/archive/final.txt');
Both source and destination paths undergo independent validation, ensuring neither can be a symlink pointing outside the trusted area.
Summary
- Atomic validation:
validatePathinsrc/tools/filesystem.tsresolves all symlinks before any filesystem operation, preventing race conditions. - Parent-directory resolution: The "SECURITY FIX" (L 58‑86) handles new files by resolving parent directories to catch symlinks in ancestor paths.
- Whitelist enforcement:
isPathAllowed(L 71‑115) checks against configurableallowedDirectoriesfromsrc/config-manager.tswith strict sub-directory validation. - Telemetry integration: Failed validations emit
server_path_validation_errorevents for security monitoring. - Universal coverage: All public handlers in
src/handlers/filesystem-handlers.tsinvoke this validation, ensuring consistent protection across read, write, move, and delete operations.
Frequently Asked Questions
How does Desktop Commander handle broken symlinks?
Desktop Commander resolves symlinks using fs.realpath during the validation phase. If a symlink is broken (points to a non-existent target), fs.realpath will throw an error, which causes validatePath to reject the path before any operation occurs. This prevents exploitation attempts using dangling symlinks that might later be re-pointed to sensitive locations.
Can the allowed directories be configured?
Yes, the whitelist is configurable via the allowedDirectories setting in the configuration, which getAllowedDirs (L 11‑25) retrieves from src/config-manager.ts. If no directories are explicitly configured, the system falls back to the user's home directory. Administrators can specify multiple allowed roots to grant access to specific project folders while maintaining sandbox boundaries.
What happens if a symlink points outside the whitelist?
If validatePath resolves a symlink and discovers the canonical path falls outside any allowed directory, it immediately throws a validation error at L 92‑100. The operation aborts before touching the filesystem, and the system logs a server_path_validation_error telemetry event. The error message indicates the path is outside the allowed directories without revealing sensitive system paths.
Is there a race condition between validation and file access?
No. Desktop Commander performs symlink resolution and whitelist validation before calling any filesystem mutating methods like fs.readFile, fs.writeFile, or fs.mkdir. By using the resolved canonical path returned from validatePath (L 101‑106) for all subsequent operations, the code eliminates the Time-of-Check Time-of-Use (TOCTOU) vulnerability that exists in check-then-act patterns.
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 →