Desktop Commander Security Measures to Prevent Symlink Attacks

Desktop Commander prevents symlink attacks by resolving all symbolic links in requested paths before validation, ensuring the canonical path lies within allowed directories.

Desktop Commander is a Model Context Protocol (MCP) client that provides sandboxed file-system access for AI agents. To prevent symlink attacks that could redirect file operations to sensitive system locations like /etc/passwd, the codebase implements a rigorous path validation mechanism centered in src/tools/filesystem.ts. This defense ensures that even if an attacker places malicious symbolic links inside permitted folders, the resolved canonical path must still fall within a configurable whitelist of safe directories.

A symlink traversal attack occurs when an attacker creates a symbolic link inside an allowed directory that points to a restricted location outside the sandbox. If the application blindly follows the symlink, it might read or write files in /etc, /root, or other sensitive areas. The danger is particularly acute when creating new files, as the target path does not yet exist and standard file existence checks fail to reveal parent-directory symlinks.

Core Defense: The validatePath Function

The heart of the protection is the validatePath function in src/tools/filesystem.ts (lines 44-106). According to the DesktopCommanderMCP source code, this function performs three decisive steps before any file operation executes:

  1. Resolve every symlink in the requested path, even when the final file does not yet exist.
  2. Normalize the absolute path and compare it against a whitelist of allowed directories.
  3. Reject the operation if the resolved path falls outside the whitelist, emitting telemetry for security monitoring.

Canonicalization with normalizePath and expandHome

Before validation begins, the system normalizes user input through helper functions. The normalizePath function (lines 34-36) expands the tilde (~) and resolves relative components like . and .., while expandHome (lines 38-42) ensures cross-platform home directory resolution. This eliminates ambiguity from user input before security checks apply.

When the target file exists, fs.realpath (lines 44-48) automatically follows all symbolic links in the path and returns the true canonical location. This handles the straightforward case where the symlink is the final path component.

The Security Fix for Non-Existent Paths

The critical innovation for preventing symlink attacks occurs when creating new files. Since fs.realpath throws ENOENT for non-existent paths, the code implements a "SECURITY FIX" (comment at line 58) between lines 58-86:

// SECURITY FIX: Handle case where file doesn't exist yet
// Resolve symlinks in the parent directory path
let currentDir = absoluteOriginal;
// Walk up until we find an existing directory
while (!fs.existsSync(currentDir)) {
    currentDir = path.dirname(currentDir);
}
// Resolve the existing parent to its real path
const resolvedExisting = fs.realpathSync(currentDir);
// Reconstruct the full path

This logic resolves the nearest existing ancestor directory, then re-attaches the non-existent leaf components. By walking up the tree and resolving symlinks at each existing level, the code guarantees that a malicious symlink like ~/projects/malicious -> /etc cannot redirect a write operation to /etc/passwd.

Whitelist Enforcement with isPathAllowed

The isPathAllowed function (lines 71-115) performs the final authorization check. It normalizes both the resolved path and each directory in allowedDirectories, then verifies:

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

If the resolved path fails these checks, the function throws an error and logs a server_path_validation_error telemetry event via the capture utility in src/utils/capture.ts.

Step-by-Step Path Validation Process

According to the filesystem.ts implementation, every file operation follows this rigorous sequence:

  1. Input processing — Public APIs like readFile or writeFile call validatePath(requestedPath).
  2. Path expansionexpandHome and normalization produce an absolute path (absoluteOriginal).
  3. Symlink resolution — If the file exists, fs.realpath returns the canonical path. If not, the parent-directory resolution logic walks up the tree, resolving symlinks at each existing level.
  4. Whitelist comparisonisPathAllowed checks if the fully resolved path lies within allowedDirectories (configured in src/config-manager.ts at lines 11-25).
  5. Rejection or approval — Unauthorized paths trigger an immediate exception with telemetry logging.
  6. Canonical path return — Validated paths are returned to the calling function, ensuring all downstream fs.readFile, fs.writeFile, or fs.mkdir operations use the safe, resolved location.

Because resolution happens before any file system call, attackers cannot exploit race conditions (time-of-check to time-of-use) to rewrite symlinks after validation.

Practical Implementation Examples

The following patterns from src/handlers/filesystem-handlers.ts demonstrate secure usage:

Reading Files with Automatic Validation

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

async function viewDocument(filename: string) {
    // validatePath automatically resolves symlinks and checks whitelist
    const result = await readFile(`~/documents/${filename}`);
    return result.content;
}

Even if ~/documents contains a symlink to /etc, the resolved path /etc/shadow would fail the whitelist check and throw an error before any read occurs.

Writing Files Safely

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

await writeFile('~/projects/config.txt', 'configuration data', 'rewrite');

If a malicious actor created ~/projects -> /etc, the parent-directory resolution would reveal that the resolved path points outside the allowed directories, preventing the write operation.

Creating Nested Directories

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

await createDirectory('~/data/2024/reports');

The validation walks up through reports, 2024, and data, resolving any symlinks at each level before creating the new directory structure.

Moving Files with Dual Validation

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

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

Both source and destination paths undergo validatePath scrutiny, ensuring neither side can exploit symlinks to access restricted areas.

Configuration and Monitoring

The security model relies on configuration-driven whitelisting via src/config-manager.ts. The getAllowedDirs function (lines 11-25) retrieves allowedDirectories from user configuration, defaulting to the home directory if unspecified.

Failed validation attempts trigger telemetry events through the capture.ts utility, allowing administrators to monitor for probing attacks or configuration errors without exposing system details to the error messages.

Summary

  • validatePath in src/tools/filesystem.ts serves as the mandatory security gateway for all file operations.
  • Parent-directory resolution (the "SECURITY FIX" at lines 58-86) prevents symlink attacks on non-existent file paths by resolving symlinks at every existing directory level.
  • Canonical path whitelisting via isPathAllowed (lines 71-115) enforces strict boundaries with sub-directory prefix matching.
  • Race-condition resistance is achieved by resolving paths immediately before file operations, eliminating TOCTOU vulnerabilities.
  • Telemetry logging via server_path_validation_error provides security monitoring without information leakage.

Frequently Asked Questions

Desktop Commander treats broken symlinks as security violations. During the resolution phase in validatePath, if fs.realpath encounters a symlink pointing to a non-existent target, it throws an error that propagates to the user. This conservative approach prevents attackers from using dangling symlinks to probe file system structure or manipulate error handling logic.

No. The architecture eliminates time-of-check to time-of-use (TOCTOU) race conditions because validatePath performs symlink resolution immediately before the file operation occurs within the same synchronous validation step. There is no window between whitelist checking and file access where an attacker could swap a legitimate file for a malicious symlink.

What directories are considered safe by default?

By default, Desktop Commander restricts all file operations to the user's home directory. Administrators can expand the sandbox by configuring the allowedDirectories array in the configuration manager (src/config-manager.ts). Each entry in this array is normalized and validated using the same isPathAllowed logic to prevent whitelist entries themselves from containing symlinks.

Does this protection work on Windows?

Yes. The isPathAllowed function (lines 71-115) includes specific logic to handle Windows drive letters and case-insensitive path comparisons. The validation normalizes paths using platform-specific separators and resolves Windows junction points and symbolic links through Node.js's fs.realpath, ensuring consistent cross-platform protection against directory traversal attacks.

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 →