Skip to content

Commit ceedae1

Browse files
ChrisDentonpietroalbini
authored andcommitted
Document Windows argument splitting
1 parent f66a096 commit ceedae1

File tree

2 files changed

+133
-2
lines changed

2 files changed

+133
-2
lines changed

library/std/src/os/windows/process.rs

+54-2
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,60 @@ pub trait CommandExt: Sealed {
199199

200200
/// Append literal text to the command line without any quoting or escaping.
201201
///
202-
/// This is useful for passing arguments to `cmd.exe /c`, which doesn't follow
203-
/// `CommandLineToArgvW` escaping rules.
202+
/// This is useful for passing arguments to applications which doesn't follow
203+
/// the standard C run-time escaping rules, such as `cmd.exe /c`.
204+
///
205+
/// # Bat files
206+
///
207+
/// Note the `cmd /c` command line has slightly different escaping rules then bat files
208+
/// themselves. If possible, it may be better to write complex arguments to a temporary
209+
/// .bat file, with appropriate escaping, and simply run that using:
210+
///
211+
/// ```no_run
212+
/// # use std::process::Command;
213+
/// # let temp_bat_file = "";
214+
/// # #[allow(unused)]
215+
/// let output = Command::new("cmd").args(["/c", &format!("\"{temp_bat_file}\"")]).output();
216+
/// ```
217+
///
218+
/// # Example
219+
///
220+
/// Run a bat script using both trusted and untrusted arguments.
221+
///
222+
/// ```no_run
223+
/// #[cfg(windows)]
224+
/// // `my_script_path` is a path to known bat file.
225+
/// // `user_name` is an untrusted name given by the user.
226+
/// fn run_script(
227+
/// my_script_path: &str,
228+
/// user_name: &str,
229+
/// ) -> Result<std::process::Output, std::io::Error> {
230+
/// use std::io::{Error, ErrorKind};
231+
/// use std::os::windows::process::CommandExt;
232+
/// use std::process::Command;
233+
///
234+
/// // Create the command line, making sure to quote the script path.
235+
/// // This assumes the fixed arguments have been tested to work with the script we're using.
236+
/// let mut cmd_args = format!(r#""{my_script_path}" "--features=[a,b,c]""#);
237+
///
238+
/// // Make sure the user name is safe. In particular we need to be
239+
/// // cautious of ascii symbols that cmd may interpret specially.
240+
/// // Here we only allow alphanumeric characters.
241+
/// if !user_name.chars().all(|c| c.is_alphanumeric()) {
242+
/// return Err(Error::new(ErrorKind::InvalidInput, "invalid user name"));
243+
/// }
244+
/// // now we've checked the user name, let's add that too.
245+
/// cmd_args.push(' ');
246+
/// cmd_args.push_str(&format!("--user {user_name}"));
247+
///
248+
/// // call cmd.exe and return the output
249+
/// Command::new("cmd.exe")
250+
/// .arg("/c")
251+
/// // surround the entire command in an extra pair of quotes, as required by cmd.exe.
252+
/// .raw_arg(&format!("\"{cmd_args}\""))
253+
/// .output()
254+
/// }
255+
/// ````
204256
#[stable(feature = "windows_process_extensions_raw_arg", since = "1.62.0")]
205257
fn raw_arg<S: AsRef<OsStr>>(&mut self, text_to_append_as_is: S) -> &mut process::Command;
206258

library/std/src/process.rs

+79
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,47 @@
8888
//! assert_eq!(b"test", output.stdout.as_slice());
8989
//! ```
9090
//!
91+
//! # Windows argument splitting
92+
//!
93+
//! On Unix systems arguments are passed to a new process as an array of strings
94+
//! but on Windows arguments are passed as a single commandline string and it's
95+
//! up to the child process to parse it into an array. Therefore the parent and
96+
//! child processes must agree on how the commandline string is encoded.
97+
//!
98+
//! Most programs use the standard C run-time `argv`, which in practice results
99+
//! in consistent argument handling. However some programs have their own way of
100+
//! parsing the commandline string. In these cases using [`arg`] or [`args`] may
101+
//! result in the child process seeing a different array of arguments then the
102+
//! parent process intended.
103+
//!
104+
//! Two ways of mitigating this are:
105+
//!
106+
//! * Validate untrusted input so that only a safe subset is allowed.
107+
//! * Use [`raw_arg`] to build a custom commandline. This bypasses the escaping
108+
//! rules used by [`arg`] so should be used with due caution.
109+
//!
110+
//! `cmd.exe` and `.bat` use non-standard argument parsing and are especially
111+
//! vulnerable to malicious input as they may be used to run arbitrary shell
112+
//! commands. Untrusted arguments should be restricted as much as possible.
113+
//! For examples on handling this see [`raw_arg`].
114+
//!
115+
//! ### Bat file special handling
116+
//!
117+
//! On Windows, `Command` uses the Windows API function [`CreateProcessW`] to
118+
//! spawn new processes. An undocumented feature of this function is that,
119+
//! when given a `.bat` file as the application to run, it will automatically
120+
//! convert that into running `cmd.exe /c` with the bat file as the next argument.
121+
//!
122+
//! For historical reasons Rust currently preserves this behaviour when using
123+
//! [`Command::new`], and escapes the arguments according to `cmd.exe` rules.
124+
//! Due to the complexity of `cmd.exe` argument handling, it might not be
125+
//! possible to safely escape some special chars, and using them will result
126+
//! in an error being returned at process spawn. The set of unescapeable
127+
//! special chars might change between releases.
128+
//!
129+
//! Also note that running `.bat` scripts in this way may be removed in the
130+
//! future and so should not be relied upon.
131+
//!
91132
//! [`spawn`]: Command::spawn
92133
//! [`output`]: Command::output
93134
//!
@@ -97,6 +138,12 @@
97138
//!
98139
//! [`Write`]: io::Write
99140
//! [`Read`]: io::Read
141+
//!
142+
//! [`arg`]: Command::arg
143+
//! [`args`]: Command::args
144+
//! [`raw_arg`]: crate::os::windows::process::CommandExt::raw_arg
145+
//!
146+
//! [`CreateProcessW`]: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
100147
101148
#![stable(feature = "process", since = "1.0.0")]
102149
#![deny(unsafe_op_in_unsafe_fn)]
@@ -611,6 +658,22 @@ impl Command {
611658
/// escaped characters, word splitting, glob patterns, variable substitution, etc.
612659
/// have no effect.
613660
///
661+
/// <div class="warning">
662+
///
663+
/// On Windows use caution with untrusted inputs. Most applications use the
664+
/// standard convention for decoding arguments passed to them. These are safe to use with `arg`.
665+
/// However some applications, such as `cmd.exe` and `.bat` files, use a non-standard way of decoding arguments
666+
/// and are therefore vulnerable to malicious input.
667+
/// In the case of `cmd.exe` this is especially important because a malicious argument can potentially run arbitrary shell commands.
668+
///
669+
/// See [Windows argument splitting][windows-args] for more details
670+
/// or [`raw_arg`] for manually implementing non-standard argument encoding.
671+
///
672+
/// [`raw_arg`]: crate::os::windows::process::CommandExt::raw_arg
673+
/// [windows-args]: crate::process#windows-argument-splitting
674+
///
675+
/// </div>
676+
///
614677
/// # Examples
615678
///
616679
/// Basic usage:
@@ -641,6 +704,22 @@ impl Command {
641704
/// escaped characters, word splitting, glob patterns, variable substitution, etc.
642705
/// have no effect.
643706
///
707+
/// <div class="warning">
708+
///
709+
/// On Windows use caution with untrusted inputs. Most applications use the
710+
/// standard convention for decoding arguments passed to them. These are safe to use with `args`.
711+
/// However some applications, such as `cmd.exe` and `.bat` files, use a non-standard way of decoding arguments
712+
/// and are therefore vulnerable to malicious input.
713+
/// In the case of `cmd.exe` this is especially important because a malicious argument can potentially run arbitrary shell commands.
714+
///
715+
/// See [Windows argument splitting][windows-args] for more details
716+
/// or [`raw_arg`] for manually implementing non-standard argument encoding.
717+
///
718+
/// [`raw_arg`]: crate::os::windows::process::CommandExt::raw_arg
719+
/// [windows-args]: crate::process#windows-argument-splitting
720+
///
721+
/// </div>
722+
///
644723
/// # Examples
645724
///
646725
/// Basic usage:

0 commit comments

Comments
 (0)