diff --git a/library/std/src/os/windows/process.rs b/library/std/src/os/windows/process.rs index 1be3acf5d436..15ab22501223 100644 --- a/library/std/src/os/windows/process.rs +++ b/library/std/src/os/windows/process.rs @@ -199,8 +199,60 @@ pub trait CommandExt: Sealed { /// Append literal text to the command line without any quoting or escaping. /// - /// This is useful for passing arguments to `cmd.exe /c`, which doesn't follow - /// `CommandLineToArgvW` escaping rules. + /// This is useful for passing arguments to applications which doesn't follow + /// the standard C run-time escaping rules, such as `cmd.exe /c`. + /// + /// # Bat files + /// + /// Note the `cmd /c` command line has slightly different escaping rules then bat files + /// themselves. If possible, it may be better to write complex arguments to a temporary + /// .bat file, with appropriate escaping, and simply run that using: + /// + /// ```no_run + /// # use std::process::Command; + /// # let temp_bat_file = ""; + /// # #[allow(unused)] + /// let output = Command::new("cmd").args(["/c", &format!("\"{temp_bat_file}\"")]).output(); + /// ``` + /// + /// # Example + /// + /// Run a bat script using both trusted and untrusted arguments. + /// + /// ```no_run + /// #[cfg(windows)] + /// // `my_script_path` is a path to known bat file. + /// // `user_name` is an untrusted name given by the user. + /// fn run_script( + /// my_script_path: &str, + /// user_name: &str, + /// ) -> Result { + /// use std::io::{Error, ErrorKind}; + /// use std::os::windows::process::CommandExt; + /// use std::process::Command; + /// + /// // Create the command line, making sure to quote the script path. + /// // This assumes the fixed arguments have been tested to work with the script we're using. + /// let mut cmd_args = format!(r#""{my_script_path}" "--features=[a,b,c]""#); + /// + /// // Make sure the user name is safe. In particular we need to be + /// // cautious of ascii symbols that cmd may interpret specially. + /// // Here we only allow alphanumeric characters. + /// if !user_name.chars().all(|c| c.is_alphanumeric()) { + /// return Err(Error::new(ErrorKind::InvalidInput, "invalid user name")); + /// } + /// // now we've checked the user name, let's add that too. + /// cmd_args.push(' '); + /// cmd_args.push_str(&format!("--user {user_name}")); + /// + /// // call cmd.exe and return the output + /// Command::new("cmd.exe") + /// .arg("/c") + /// // surround the entire command in an extra pair of quotes, as required by cmd.exe. + /// .raw_arg(&format!("\"{cmd_args}\"")) + /// .output() + /// } + /// ```` #[stable(feature = "windows_process_extensions_raw_arg", since = "1.62.0")] fn raw_arg>(&mut self, text_to_append_as_is: S) -> &mut process::Command; diff --git a/library/std/src/process.rs b/library/std/src/process.rs index b84d5f11954d..69cc61b30efe 100644 --- a/library/std/src/process.rs +++ b/library/std/src/process.rs @@ -88,6 +88,47 @@ //! assert_eq!(b"test", output.stdout.as_slice()); //! ``` //! +//! # Windows argument splitting +//! +//! On Unix systems arguments are passed to a new process as an array of strings +//! but on Windows arguments are passed as a single commandline string and it's +//! up to the child process to parse it into an array. Therefore the parent and +//! child processes must agree on how the commandline string is encoded. +//! +//! Most programs use the standard C run-time `argv`, which in practice results +//! in consistent argument handling. However some programs have their own way of +//! parsing the commandline string. In these cases using [`arg`] or [`args`] may +//! result in the child process seeing a different array of arguments then the +//! parent process intended. +//! +//! Two ways of mitigating this are: +//! +//! * Validate untrusted input so that only a safe subset is allowed. +//! * Use [`raw_arg`] to build a custom commandline. This bypasses the escaping +//! rules used by [`arg`] so should be used with due caution. +//! +//! `cmd.exe` and `.bat` use non-standard argument parsing and are especially +//! vulnerable to malicious input as they may be used to run arbitrary shell +//! commands. Untrusted arguments should be restricted as much as possible. +//! For examples on handling this see [`raw_arg`]. +//! +//! ### Bat file special handling +//! +//! On Windows, `Command` uses the Windows API function [`CreateProcessW`] to +//! spawn new processes. An undocumented feature of this function is that, +//! when given a `.bat` file as the application to run, it will automatically +//! convert that into running `cmd.exe /c` with the bat file as the next argument. +//! +//! For historical reasons Rust currently preserves this behaviour when using +//! [`Command::new`], and escapes the arguments according to `cmd.exe` rules. +//! Due to the complexity of `cmd.exe` argument handling, it might not be +//! possible to safely escape some special chars, and using them will result +//! in an error being returned at process spawn. The set of unescapeable +//! special chars might change between releases. +//! +//! Also note that running `.bat` scripts in this way may be removed in the +//! future and so should not be relied upon. +//! //! [`spawn`]: Command::spawn //! [`output`]: Command::output //! @@ -97,6 +138,12 @@ //! //! [`Write`]: io::Write //! [`Read`]: io::Read +//! +//! [`arg`]: Command::arg +//! [`args`]: Command::args +//! [`raw_arg`]: crate::os::windows::process::CommandExt::raw_arg +//! +//! [`CreateProcessW`]: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw #![stable(feature = "process", since = "1.0.0")] #![deny(unsafe_op_in_unsafe_fn)] @@ -611,6 +658,22 @@ impl Command { /// escaped characters, word splitting, glob patterns, variable substitution, etc. /// have no effect. /// + ///
+ /// + /// On Windows use caution with untrusted inputs. Most applications use the + /// standard convention for decoding arguments passed to them. These are safe to use with `arg`. + /// However some applications, such as `cmd.exe` and `.bat` files, use a non-standard way of decoding arguments + /// and are therefore vulnerable to malicious input. + /// In the case of `cmd.exe` this is especially important because a malicious argument can potentially run arbitrary shell commands. + /// + /// See [Windows argument splitting][windows-args] for more details + /// or [`raw_arg`] for manually implementing non-standard argument encoding. + /// + /// [`raw_arg`]: crate::os::windows::process::CommandExt::raw_arg + /// [windows-args]: crate::process#windows-argument-splitting + /// + ///
+ /// /// # Examples /// /// Basic usage: @@ -641,6 +704,22 @@ impl Command { /// escaped characters, word splitting, glob patterns, variable substitution, etc. /// have no effect. /// + ///
+ /// + /// On Windows use caution with untrusted inputs. Most applications use the + /// standard convention for decoding arguments passed to them. These are safe to use with `args`. + /// However some applications, such as `cmd.exe` and `.bat` files, use a non-standard way of decoding arguments + /// and are therefore vulnerable to malicious input. + /// In the case of `cmd.exe` this is especially important because a malicious argument can potentially run arbitrary shell commands. + /// + /// See [Windows argument splitting][windows-args] for more details + /// or [`raw_arg`] for manually implementing non-standard argument encoding. + /// + /// [`raw_arg`]: crate::os::windows::process::CommandExt::raw_arg + /// [windows-args]: crate::process#windows-argument-splitting + /// + ///
+ /// /// # Examples /// /// Basic usage: