Are batch scripts a security vulnerability?

A few weeks back a CVE landed that affected practically every language's standard library on Windows. RyotaK has a very detailed write up that you should definitely read if you haven't already: BatBadBut: You can't securely execute commands on Windows. The short version is that sending untrusted arguments to even a trusted batch script is a vulnerability because the shell gets involved before the script is even run (more on that in a sec).

However, different languages reacted to it differently:

A question I've been asked a fair bit since then is why there were this range of responses. I can't speak for other languages but I can try to explain why Rust considered this a library vulnerability and speculate why others may be either reluctant to patch or else not consider this critical enough to fix.

So why does Rust think this is a library vulnerability? Let me try to explain...

Running scripts

On Linux a bash script can be run like this:

bash script.sh one two three

This runs script.sh with the arguments one, two and three. If you use an exec* function to execute bash, and not a shell, then the arguments are passed directly to the script.

Ok but imagine for a second that this way of running bash scripts didn't exist. Well there is another way:

bash -c "script.sh one two three"

This will do the same thing but the big difference is that the arguments are evaluated by bash. In this case it's harmless but you can see how it could be used for more nefarious purposes...

bash -c "script.sh && rm -rf ./"

This is in essence the issue with running .bat files on Windows. cmd.exe requires you to use cmd.exe /c to run batch scripts meaning the whole command is evaluated by the the script interpreter.

CreateProcess and standard library vulnerabilities

CreateProcess is the Windows API used to spawn a new process. It has a fun, undocumented, feature that it'll automatically run .bat (and .cmd) files using cmd.exe /c without even telling you. I say "undocumented" but this is actually anti-documented:

To run a batch file, you must start the command interpreter; set lpApplicationName to cmd.exe and set lpCommandLine to the following arguments: /c plus the name of the batch file.

It's telling you that you must do the cmd.exe /c dance manually. Only this isn't true and it's easy for people to accidentally discover it isn't true.

So if you run the following in Rust 1.0:

Command::new("script.bat").arg("&calc").spawn();

It will run cmd /c script.bat &calc, causing calc.exe to be spawned. The same was true of any language, and still is for those who haven't patched. That's the library vulnerability.

Note that this has nothing to do with script.bat itself. You may have a batch script that the foremost experts in all the land have verified is safe and secure, yet it would still be vulnerable because the script is not involved at all. In short this is somewhat akin to passing an untrusted string to eval in many languages.

This also has nothing to do with the way arguments are passed to new processes on Windows (although that is relevant to the workaround). If standard libraries (or the OS!) automatically ran bash scripts using bash -c on Linux then they would also be vulnerable. Unless of course the library function, like system, documents that commands are run in a shell.

The fix here would be for cmd.exe to be able to have some way of passing on arguments to scripts without first evaluating them as commands to run. However language standard libraries obviously cannot implement that. They can instead workaround the issue by escaping arguments, as RyotaK's article explains.

So why didn't some languages patch this?

I would reiterate here that I can't speak for other languages, only offer speculation as to why their security teams would interpret the same issue differently. With that disclaimer out of the way, I'd suggest it's some combination of the following:

  1. They disagree that this issue is as critical as claimed.
    This is after all a feature of Windows that has existed for decades and isn't exactly a secret, even if undocumented. It could also be argued that if there's any fault then the blame lies with the OS and not with libraries that simply wrap OS APIs.

  2. They didn't want to make a breaking change.
    The workaround by Rust and others might be considered a breaking change, however hard it tries to avoid any real-world breakage. Also some (hopefully few) people may have actually been counting on the command injection for some reason. Those people could've just used cmd /c directly but maybe they didn't want to.

  3. They already documented that untrusted arguments are a vulnerability.
    A broad enough disclaimer can cover this and any future CVE related to process arguments, all without ever needing to update their standard library or even documentation.

A niche issue?

I'll end by admitting this is perhaps a more niche issue than it's 10/10 critical rating on github might suggest even if I agree that it is indeed a library vulnerability. I suspect most people aren't in fact passing untrusted input (e.g. from the internet) to batch scripts. Or if they are then they at least validate the input.

But for those people who are unavoidably affected, having some way to avoid command injection attacks is undoubtedly critical.