Feb 10, 2026·8 min read·47 visits
Windows implicitly uses 'cmd.exe' to run batch files even when you don't ask for a shell. Most programming languages didn't account for 'cmd.exe's' bizarre parsing rules, allowing attackers to break out of argument quotes and execute commands. CVSS 9.8.
A critical command injection vulnerability affecting multiple programming language runtimes on Windows. It arises from an impedance mismatch between how runtimes escape arguments for process execution and how the Windows operating system implicitly handles batch files via 'cmd.exe'. This flaw turns standard argument passing into arbitrary remote code execution.
Picture this: You are a developer. You are writing a secure application in Rust, or Node.js, or PHP. You need to spawn a child process. You are a responsible adult, so you don't use system() or exec(). You use the safe, array-based process spawning functions like Command::new() or child_process.spawn(). You pass arguments as an array, distinct from the command, specifically to avoid command injection. You pat yourself on the back. You are safe.
Then, you deploy on Windows. And suddenly, your 'safe' function isn't just spawning a process. It's inadvertently inviting a chaotic, 30-year-old demon into your memory space. That demon is cmd.exe.
CVE-2024-3566, dubbed 'BatBadBut', isn't a bug in your code, and it's arguably not even a bug in Windows (according to Microsoft). It is a catastrophic misunderstanding between modern programming languages and the ancient, eldritch horror that is Windows batch file execution. When you try to run a file on Windows, and that file happens to end in .bat or .cmd, Windows doesn't just run it. It effectively says, 'Oh, I can't run text files! Let me pass this to cmd.exe for you.'
The problem? The standard library of your favorite programming language formatted your arguments for the Windows Kernel (CreateProcess), not for the command prompt. And those two parsers speak entirely different languages.
To understand why this breaks, you have to understand the Schizophrenia of Windows argument parsing. In the Unix world, arguments are passed as an array of strings. Clean. Simple. In Windows, the kernel takes a single command-line string. It is up to the application to parse that string back into an array. Most executables use the standard Microsoft C Runtime (CRT) rules for this. These rules are generally sane: you wrap strings in double quotes, and if you have a double quote inside the string, you escape it with a backslash (\").
Most language runtimes (Rust, Node, etc.) implement this CRT escaping logic perfectly. They assume that whatever they spawn will follow these rules. But here enters the villain: cmd.exe. The Windows Command Prompt does not follow CRT rules. It has its own 'unique' parsing logic that dates back to the DOS era. It doesn't respect backslashes as escape characters in the same way. It uses the caret (^) for escaping, and it parses special redirection characters (&, |, <, >) aggressively.
So, here is the setup for disaster: Your runtime sees an argument like "&calc.exe. It thinks, 'I need to escape that quote so it stays a string.' It transforms it into \"&calc.exe and wraps it in outer quotes. The final string sent to the OS looks like "\"&calc.exe". The runtime thinks it sent a safe, escaped string.
But because the target is a .bat file, Windows silently invokes cmd.exe. When cmd.exe parses "\"&calc.exe", it sees the first quote (start string), then it sees \ (just a literal backslash, not an escape), and then it sees " (end string). The string is now closed. What follows next is &calc.exe—which cmd.exe interprets as 'run the previous command, AND THEN run Calculator'.
Let's look at the logic gap in a typical runtime before the patch. The standard Windows argument construction generally looks like this pseudo-code:
function build_command_line(args) {
let cmd_line = "";
for (let arg of args) {
// CRT Escaping Rule: Escape quotes with backslash
let escaped = arg.replace(/"/g, '\\"');
cmd_line += ` "${escaped}"`;
}
return cmd_line;
}This logic is mathematically correct for 99% of Windows executables. If you run main.exe, it works. But CreateProcess in Windows has a specific behavior: if the executable path ends in .bat or .cmd, and no extension was provided, or if it was provided explicitly, it spins up cmd.exe /c.
The vulnerability exists because the runtime assumes the destination parser is CRT-compliant. It fails to check if the destination is a batch file. The fix, implemented across Rust, Node, and PHP, involves a terrifying amount of complexity. They now have to check if the command being executed ends in .bat or .cmd. If it does, they have to abandon standard escaping and implement a custom, nightmare-fuel escaping logic specifically for cmd.exe.
Here is a visualization of the failure flow:
Exploiting this requires a specific set of circumstances, but they are more common than you'd think. You need an application running on Windows that allows you to control an argument passed to a process spawning function. Crucially, the process being spawned must resolve to a batch file. This might happen if the developer explicitly calls wrapper.bat, or if they call wrapper and wrapper.bat exists in the system PATH.
Let's assume a vulnerable Node.js application:
const { spawn } = require('child_process');
// User input controls the argument
let userInput = '"&calc.exe';
spawn('test.bat', [userInput]);The exploit payload is deceptively simple. We aren't overflowing buffers on the heap; we are confusing the parser logic. The payload relies on closing the string literal that the runtime tried to create.
The Payload: "&whoami
The Transformation:
\. Result: \"&whoami."\"&whoami".cmd.exe /c test.bat ...": Open quote.\: Literal backslash (CMD doesn't care about backslashes inside quotes usually, but here parsing gets weird with how the start quote interacts).": Close quote. (The backslash failed to escape this for CMD).&: Command separator.whoami: Executes command.If you want to be nastier and avoid immediate crashes or syntax errors in the batch file execution, you might pad it or redirect output. But the core concept is using the quote-escaping mismatch to terminate the argument early.
This is a CVSS 9.8 for a reason. Remote Code Execution (RCE) is the endgame. If an attacker can trigger this, they are running code with the privileges of your web server or application. On Windows, this often means SYSTEM or a highly privileged service account, because let's be honest, Windows permission management is hard and people get lazy.
Consider the scenarios:
npm, cargo, or composer often execute scripts. If a malicious package includes a weirdly named binary or script, it could trigger this during installation.The cynical reality is that this vulnerability has existed for decades. It was only assigned a CVE recently because security researchers finally convinced the language maintainers that "Windows is just broken" wasn't a valid excuse for ignoring the problem anymore.
The mitigation for BatBadBut is messy. Language maintainers (Rust, Node, PHP) have released patches that detect if a command targets a .bat or .cmd file. If so, they apply custom escaping logic. They escape special shell characters (%, !, ^, ", <, >, |, &) manually.
However, this detection is brittle. It relies on knowing the file extension. If you execute a command without an extension, and Windows resolves it to a .bat file via PATH, the runtime might not know in time to apply the escaping. This is why the 'Fix' is only partial.
Actionable Advice for Developers:
my-script; call my-script.exe. If you must call a batch file, call my-script.bat so the patched runtime detects it.& or " anywhere near your process spawning logic.Remember: On Windows, CreateProcess is a leaky abstraction. Treat it with the suspicion it deserves.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Node.js OpenJS Foundation | < 18.20.2 | 18.20.2 |
Node.js OpenJS Foundation | < 20.12.2 | 20.12.2 |
Node.js OpenJS Foundation | < 21.7.2 | 21.7.2 |
Rust Rust Foundation | < 1.77.2 | 1.77.2 |
PHP PHP Group | 8.x | 8.3.5 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-78 |
| CVSS v3.1 | 9.8 (Critical) |
| Attack Vector | Network (Arguments passed remotely) |
| EPSS Score | 0.053 (~5.3%) |
| Exploit Status | PoC Available |
| KEV Listed | No |
Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')