How Desktop Commander MCP Prevents Symlink Traversal Attacks in File Operations

Desktop Commander MCP blocks symlink traversal attacks by resolving every path to its canonical location using fs.realpath, validating parent directories for non-existent targets, and enforcing strict allowed-directory policies before any filesystem operation executes.

Symlink traversal attacks allow malicious actors to read or write files outside intended directories by exploiting symbolic links. In the Desktop Commander MCP repository, the filesystem security layer ensures that AI agents can only access explicitly permitted locations, even when attackers craft malicious symlink chains.

Real-Path Resolution in validatePath

The core defense resides in src/tools/filesystem.ts, specifically within the validatePath function. When any file operation receives a path, the function immediately calls fs.realpath on the absolute path to resolve every component—including intermediate symlinks—to its canonical target.

According to the Desktop Commander MCP source code at lines 39-47 in src/tools/filesystem.ts, this resolution happens before any directory check:

// From src/tools/filesystem.ts - validatePath resolves symlinks first
const realPath = await fs.realpath(absolutePath);
// If path exists, realPath contains the canonical location
// If it contains symlinks, they are fully resolved

This ensures that if an attacker places a symlink inside an allowed directory that points to /etc/passwd or C:\Windows\System32, the validation system sees the true destination path rather than the symlink location.

Parent-Directory Validation for Non-Existent Targets

Creating new files presents a unique security challenge: the target path does not exist yet, so fs.realpath cannot resolve the final component. Desktop Commander MCP handles this by implementing a parent-directory fallback mechanism.

As implemented in src/tools/filesystem.ts lines 58-86, the function walks up the directory tree until it finds the deepest existing ancestor, resolves that to its real path, then reconstructs the intended path:

// Security-critical: Resolve parent when target doesn't exist
let current = path.dirname(absolutePath);
while (!(await exists(current))) {
  current = path.dirname(current);
}
const realParent = await fs.realpath(current);
// Reconstruct the full path from the validated parent

This prevents attackers from creating a non-existent sub-path through a symlink that points outside the allowed tree. Without this check, an attacker could create allowed_dir/symlink_to_etc/new_file and bypass restrictions.

Allowed-Directory Enforcement with isPathAllowed

After path resolution, the isPathAllowed function (located in src/tools/filesystem.ts lines 77-100) performs the authorization check. It compares the resolved canonical path against the configurable allowedDirectories list.

The comparison uses normalized, lower-cased paths with trailing-separator guards to prevent partial-name matches:

  • Exact boundary checking: Ensures /allowed/path matches /allowed/path/file but not /allowed/path_backup
  • Case normalization: Handles case-insensitive filesystems correctly
  • Separator normalization: Handles both forward slashes and backslashes on Windows

If the resolved path is not a sub-directory of any configured allowed entry, the system throws a validation error.

Security Event Logging and Error Handling

When path validation fails, Desktop Commander MCP logs a server_path_validation_error event and rejects the operation with a descriptive error message. As shown in lines 92-99 of src/tools/filesystem.ts, this creates an audit trail for security monitoring:

// Security violation handling
if (!isAllowed) {
  server.sendLoggingMessage({
    level: 'error',
    data: `Path validation failed: ${normalizedPath}`
  });
  throw new Error(`Path not allowed: ${requestedPath}`);
}

This immediate rejection ensures that no filesystem operation proceeds on paths outside the sandboxed area.

Comprehensive Security Testing

The repository includes a dedicated test suite at test/test-symlink-security.js that validates these protections against realistic attack scenarios. Lines 13-30 create both directory and file symlinks pointing outside the allowed tree and assert that validatePath correctly blocks them:


# Run the symlink security test suite

npm run test:test-symlink-security

# or directly with Node:

node test/test-symlink-security.js

These tests simulate attacks where:

  • A symlink inside the allowed directory points to /etc
  • A nested symlink chain attempts to escape the sandbox
  • Non-existent paths are created through malicious symlinks

All scenarios verify that the canonical path resolution correctly identifies the true target location and rejects unauthorized access.

Implementation Example: Configuring Allowed Directories

To leverage these security features, configure the allowedDirectories setting before performing file operations:

import { configManager } from './config-manager.js';
import { validatePath } from './tools/filesystem.js';

// Restrict operations to a specific sandbox
await configManager.setValue('allowedDirectories', ['/home/user/safe_projects']);

// This succeeds - real path stays within allowed directory
const safeFile = await validatePath('safe_projects/readme.md');

// This fails - resolves to /etc/passwd via symlink
try {
  await validatePath('safe_projects/link_to_passwd');
} catch (e) {
  console.error(e.message); // "Path not allowed: ..."
}

Summary

  • Canonical resolution: fs.realpath in src/tools/filesystem.ts resolves all symlinks before directory checks occur.
  • Parent validation: For non-existent targets, the system validates the deepest existing ancestor to prevent creation through malicious symlinks.
  • Strict boundary enforcement: isPathAllowed uses normalized path comparison with trailing-separator guards to prevent partial directory name matches.
  • Audit logging: Failed validations trigger server_path_validation_error events for security monitoring.
  • Automated verification: The test-symlink-security.js suite continuously validates protections against directory traversal attacks.

Frequently Asked Questions

Desktop Commander MCP detects symlink attacks by calling fs.realpath in the validatePath function before any filesystem operation. This resolves the supplied path to its canonical location, following all symbolic links in the chain. If the resolved path falls outside the configured allowedDirectories, the system blocks the operation and logs a validation error.

When creating a new file where the target does not yet exist, the system uses a parent-directory fallback. It walks up the directory tree to find the deepest existing ancestor, resolves that to its real path using fs.realpath, then reconstructs the intended path. If any component resolves outside the allowed directories, the operation fails immediately, preventing attackers from writing to sensitive locations through symlink chains.

Can an attacker bypass allowed-directory checks using case sensitivity tricks?

No. The isPathAllowed function in src/tools/filesystem.ts normalizes paths to lowercase before comparison and handles trailing separators explicitly. This prevents bypasses through case variations (e.g., Allowed_DIR vs allowed_dir) or partial directory name matches (e.g., /allowed matching /allowed_backup).

The security design is documented in SECURITY.md lines 23-27, which identifies directory-restriction bypass via symlinks as a known risk and describes the mitigation strategy. Implementation details appear in the README.md security section, providing user-facing guidance on configuring allowedDirectories and understanding the validation workflow.

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 →