5

A while ago there was a security advisory published for Rust that the standard library's Command API was not sufficiently handling arguments passed to cmd.exe and .bat files such that malicious arguments could invoke arbitrary shell code. This was fixed in Rust 1.77.2.

Reading through the advisory as well as the documentation on Command::arg and the Windows argument splitting section of std::process, I can see the core issue originates from Windows passing arguments via a flat string and the parsing of that string can differ based on the executable, but I have not seen an explanation of what inputs are a concern for cmd.exe nor how Command::arg mitigates them.

For example, if I have a batch file and I provide a potentially malicious argument:

Command::new("my_script.bat")
    .arg(&untrusted_user_input)
    .spawn();
  • Is this safe with the 1.77.2 mitigations or is it still vulnerable?
  • What kind of escape logic is applied to the argument?
  • What problems can occur if I were to use .raw_arg instead?
  • What input could be "inescapable" and thus return an InvalidInput error?

I've found it hard to find good resources on cmd.exe parsing. There's How does the Windows Command Interpreter (CMD.EXE) parse scripts? which may have the answers, but its incredibly dense and not clear which parts are relevant (whole script vs just arguments). There's also Does the Windows "cmd.exe" parse arguments differently? which shows that /C can behave in unintuitive ways, but its not clear if/how that related to calling a batch script.

The Rust documentation telling me to "use caution with untrusted inputs" and "validate untrusted input so that only a safe subset is allowed" does not provide me with any actionable guidance if I do need to handle untrusted inputs.

1

1 Answer 1

1

I try to answer some of your questions.

The way Rust escapes the arguments seems to be mainly for calling EXE files
This is only necessary because Rust uses 'cmd.exe /c' to start an EXE file.
This is why these complex escaping rules are necessary.
However, using cmd.exe in the first place was a poor design decision, as it complicates the escaping rules yet fails to escape batch file arguments correctly.
It also fails to execute internal batch commands such as 'echo' or 'set', or more complex commands.

The explanation when a batch script is started!

What input could be "inescapable" and thus return an InvalidInput error?

  • If your input contains \r, \n or NUL. Newline and carriage return are technically legal characters in an argument for batch files, but extremely hard to handle in a safe way. A NUL characters can't be handled in any way with batch

What kind of escape logic is applied to the argument?

Results from tests, see below

  • Quotes get doubled (It's more or less wrong, but it avoids problems later in expansion of %* or %1)

  • Backslashes get doubled if in front of quotes (completely wrong, they don't have a special meaning in batch)

  • Percent signs are translated to %%cd:~,% (nice trick to avoid unexpected variable expansions)

  • The argument is enclosed in quotes, if there is at least one non white listed character in the argument, white listed are letters, digits and #$*+-./:?@\_

What problems can occur if I were to use .raw_arg instead?

  • raw_arg opens all kinds of injection problems, if you don't escape the arguments it will fail.
    This raw_arg can be used to call a batch script: let my_raw_arg = "\"my_script.bat 1 ^&\"& 3\"";
    But for the batch itself it's impossible to access the arguments with %* or %1

If you are worried about security, then it's unimportant if rust can escape the arguments.
The majority of batch files will fail to handle arguments securely, as it's really hard to build a bullet proof solution with batch.

Avoid using batch files in the first place.

I've tested with: rustc 1.87.0

use std::process::Command;

fn main()
{
    let untrusted_user_input = "1 & 2 | 3 < 4 > 5 ^ 6 % 7 ! 8 \\ 9 / 10 \" 11 \\ \" 12 \\\" 13 \\\\\\\" END";
    println!("Using input :\n        {}", untrusted_user_input);

    let output = Command::new("my_script.bat")
        .arg(untrusted_user_input)
        .output()
        .expect("Failed to execute command");

    // Show the batch output
    println!("Batch output: {}", String::from_utf8_lossy(&output.stdout));
    if !output.stderr.is_empty() {
        println!("Stderr: {}", String::from_utf8_lossy(&output.stderr));
    }
}

and my_script.bat

@echo off
setlocal EnableDelayedExpansion

set "prompt=#"
echo on
REM # %* #
@echo off
echo:
echo # !cmdcmdline!

Output:

Using input :
        1 & 2 | 3 < 4 > 5 ^ 6 %OS% 7 ! 8 \ 9 / 10 " 11 \ " 12 \" 13 \\\" END
Batch output:
#REM # "1 & 2 | 3 < 4 > 5 ^ 6 %OS% 7 ! 8 \ 9 / 10 "" 11 \ "" 12 \\"" 13 \\\\\\"" END" #

# # cmd.exe /e:ON /v:OFF /d /c ""Z:\rust\my_script.bat" "1 & 2 | 3 < 4 > 5 ^ 6 %%cd:~,%OS%%cd:~,% 7 ! 8 \ 9 / 10 "" 11 \ "" 12 \\"" 13 \\\\\\"" END""
Sign up to request clarification or add additional context in comments.

3 Comments

Jeez quotes are weird. So does this mean that quotes in the input will always be mangled? I would've expected an error if a parameter could not be passed faithfully, but I agree with your assessment.
Quoting and escaping between different languages usually gets tricky.
@kmdreko Yes, the quotes are always doubled. However, this could be beneficial if you have control over the batch script, as it ensures safe input handling. You only need to undo the doubling with set "argv=!argv:""="!"

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.