Skip to content

Commit d2c4535

Browse files
committed
Auto merge of #126391 - tbu-:pr_command_env_equals, r=cuviper
Validate environment variable names in `std::process` Make sure that they're not empty and do not contain `=` signs beyond the first character. This prevents environment variable injection, because previously, setting the `PATH=/opt:` variable to `foobar` would lead to the `PATH` variable being overridden. Fixes #122335. try-job: x86_64-msvc
2 parents ecf2d1f + 6e57e34 commit d2c4535

File tree

7 files changed

+113
-46
lines changed

7 files changed

+113
-46
lines changed

library/std/src/ffi/mod.rs

-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@
144144
//!
145145
//! [Unicode scalar value]: https://www.unicode.org/glossary/#unicode_scalar_value
146146
//! [Unicode code point]: https://www.unicode.org/glossary/#code_point
147-
//! [`env::set_var()`]: crate::env::set_var "env::set_var"
148147
//! [`env::var_os()`]: crate::env::var_os "env::var_os"
149148
//! [unix.OsStringExt]: crate::os::unix::ffi::OsStringExt "os::unix::ffi::OsStringExt"
150149
//! [`from_vec`]: crate::os::unix::ffi::OsStringExt::from_vec "os::unix::ffi::OsStringExt::from_vec"

library/std/src/process/tests.rs

+36
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,42 @@ fn test_interior_nul_in_env_value_is_error() {
377377
}
378378
}
379379

380+
#[test]
381+
fn test_empty_env_key_is_error() {
382+
match env_cmd().env("", "value").spawn() {
383+
Err(e) => assert_eq!(e.kind(), ErrorKind::InvalidInput),
384+
Ok(_) => panic!(),
385+
}
386+
}
387+
388+
#[test]
389+
fn test_interior_equals_in_env_key_is_error() {
390+
match env_cmd().env("has=equals", "value").spawn() {
391+
Err(e) => assert_eq!(e.kind(), ErrorKind::InvalidInput),
392+
Ok(_) => panic!(),
393+
}
394+
}
395+
396+
#[test]
397+
fn test_env_leading_equals() {
398+
let mut cmd;
399+
if cfg!(not(target_os = "windows")) {
400+
cmd = env_cmd();
401+
} else {
402+
cmd = Command::new("cmd");
403+
cmd.arg("/c");
404+
cmd.arg("echo =RUN_TEST_LEADING_EQUALS=%=RUN_TEST_LEADING_EQUALS%");
405+
}
406+
cmd.env("=RUN_TEST_LEADING_EQUALS", "789=012");
407+
let result = cmd.output().unwrap();
408+
let output = String::from_utf8_lossy(&result.stdout).to_string();
409+
410+
assert!(
411+
output.contains("=RUN_TEST_LEADING_EQUALS=789=012"),
412+
"didn't find =RUN_TEST_LEADING_EQUALS inside of:\n\n{output}",
413+
);
414+
}
415+
380416
/// Tests that process creation flags work by debugging a process.
381417
/// Other creation flags make it hard or impossible to detect
382418
/// behavioral changes in the process.

library/std/src/sys/pal/unix/process/process_common.rs

+25-4
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ pub struct Command {
9696
uid: Option<uid_t>,
9797
gid: Option<gid_t>,
9898
saw_nul: bool,
99+
saw_invalid_env_key: bool,
99100
closures: Vec<Box<dyn FnMut() -> io::Result<()> + Send + Sync>>,
100101
groups: Option<Box<[gid_t]>>,
101102
stdin: Option<Stdio>,
@@ -190,6 +191,7 @@ impl Command {
190191
uid: None,
191192
gid: None,
192193
saw_nul,
194+
saw_invalid_env_key: false,
193195
closures: Vec::new(),
194196
groups: None,
195197
stdin: None,
@@ -214,6 +216,7 @@ impl Command {
214216
uid: None,
215217
gid: None,
216218
saw_nul,
219+
saw_invalid_env_key: false,
217220
closures: Vec::new(),
218221
groups: None,
219222
stdin: None,
@@ -276,8 +279,17 @@ impl Command {
276279
self.create_pidfd
277280
}
278281

279-
pub fn saw_nul(&self) -> bool {
280-
self.saw_nul
282+
pub fn validate_input(&self) -> io::Result<()> {
283+
if self.saw_invalid_env_key {
284+
Err(io::const_io_error!(
285+
io::ErrorKind::InvalidInput,
286+
"env key empty or equals sign found in env key",
287+
))
288+
} else if self.saw_nul {
289+
Err(io::const_io_error!(io::ErrorKind::InvalidInput, "nul byte found in provided data"))
290+
} else {
291+
Ok(())
292+
}
281293
}
282294

283295
pub fn get_program(&self) -> &OsStr {
@@ -358,7 +370,7 @@ impl Command {
358370

359371
pub fn capture_env(&mut self) -> Option<CStringArray> {
360372
let maybe_env = self.env.capture_if_changed();
361-
maybe_env.map(|env| construct_envp(env, &mut self.saw_nul))
373+
maybe_env.map(|env| construct_envp(env, &mut self.saw_nul, &mut self.saw_invalid_env_key))
362374
}
363375

364376
#[allow(dead_code)]
@@ -423,9 +435,18 @@ impl CStringArray {
423435
}
424436
}
425437

426-
fn construct_envp(env: BTreeMap<OsString, OsString>, saw_nul: &mut bool) -> CStringArray {
438+
fn construct_envp(
439+
env: BTreeMap<OsString, OsString>,
440+
saw_nul: &mut bool,
441+
saw_invalid_env_key: &mut bool,
442+
) -> CStringArray {
427443
let mut result = CStringArray::with_capacity(env.len());
428444
for (mut k, v) in env {
445+
if k.is_empty() || k.as_bytes()[1..].contains(&b'=') {
446+
*saw_invalid_env_key = true;
447+
continue;
448+
}
449+
429450
// Reserve additional space for '=' and null terminator
430451
k.reserve_exact(v.len() + 2);
431452
k.push("=");

library/std/src/sys/pal/unix/process/process_fuchsia.rs

+3-11
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,7 @@ impl Command {
1717
) -> io::Result<(Process, StdioPipes)> {
1818
let envp = self.capture_env();
1919

20-
if self.saw_nul() {
21-
return Err(io::const_io_error!(
22-
io::ErrorKind::InvalidInput,
23-
"nul byte found in provided data",
24-
));
25-
}
20+
self.validate_input()?;
2621

2722
let (ours, theirs) = self.setup_io(default, needs_stdin)?;
2823

@@ -37,11 +32,8 @@ impl Command {
3732
}
3833

3934
pub fn exec(&mut self, default: Stdio) -> io::Error {
40-
if self.saw_nul() {
41-
return io::const_io_error!(
42-
io::ErrorKind::InvalidInput,
43-
"nul byte found in provided data",
44-
);
35+
if let Err(err) = self.validate_input() {
36+
return err;
4537
}
4638

4739
match self.setup_io(default, true) {

library/std/src/sys/pal/unix/process/process_unix.rs

+13-18
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@ use libc::{c_int, pid_t};
1010
)))]
1111
use libc::{gid_t, uid_t};
1212

13-
use crate::io::{self, Error, ErrorKind};
1413
use crate::num::NonZero;
1514
use crate::sys::cvt;
1615
#[cfg(target_os = "linux")]
1716
use crate::sys::pal::unix::linux::pidfd::PidFd;
1817
use crate::sys::process::process_common::*;
19-
use crate::{fmt, mem, sys};
18+
use crate::{fmt, io, mem, sys};
2019

2120
cfg_if::cfg_if! {
2221
// This workaround is only needed for QNX 7.0 and 7.1. The bug should have been fixed in 8.0
@@ -60,12 +59,7 @@ impl Command {
6059

6160
let envp = self.capture_env();
6261

63-
if self.saw_nul() {
64-
return Err(io::const_io_error!(
65-
ErrorKind::InvalidInput,
66-
"nul byte found in provided data",
67-
));
68-
}
62+
self.validate_input()?;
6963

7064
let (ours, theirs) = self.setup_io(default, needs_stdin)?;
7165

@@ -146,7 +140,7 @@ impl Command {
146140
);
147141
let errno = i32::from_be_bytes(errno.try_into().unwrap());
148142
assert!(p.wait().is_ok(), "wait() should either return Ok or panic");
149-
return Err(Error::from_raw_os_error(errno));
143+
return Err(io::Error::from_raw_os_error(errno));
150144
}
151145
Err(ref e) if e.is_interrupted() => {}
152146
Err(e) => {
@@ -175,8 +169,8 @@ impl Command {
175169
// allowed to exist in dead code), but it sounds bad, so we go out of our
176170
// way to avoid that all-together.
177171
#[cfg(any(target_os = "tvos", target_os = "watchos"))]
178-
const ERR_APPLE_TV_WATCH_NO_FORK_EXEC: Error = io::const_io_error!(
179-
ErrorKind::Unsupported,
172+
const ERR_APPLE_TV_WATCH_NO_FORK_EXEC: io::Error = io::const_io_error!(
173+
io::ErrorKind::Unsupported,
180174
"`fork`+`exec`-based process spawning is not supported on this target",
181175
);
182176

@@ -219,7 +213,7 @@ impl Command {
219213
thread::sleep(delay);
220214
} else {
221215
return Err(io::const_io_error!(
222-
ErrorKind::WouldBlock,
216+
io::ErrorKind::WouldBlock,
223217
"forking returned EBADF too often",
224218
));
225219
}
@@ -234,8 +228,8 @@ impl Command {
234228
pub fn exec(&mut self, default: Stdio) -> io::Error {
235229
let envp = self.capture_env();
236230

237-
if self.saw_nul() {
238-
return io::const_io_error!(ErrorKind::InvalidInput, "nul byte found in provided data",);
231+
if let Err(err) = self.validate_input() {
232+
return err;
239233
}
240234

241235
match self.setup_io(default, true) {
@@ -561,7 +555,7 @@ impl Command {
561555
thread::sleep(delay);
562556
} else {
563557
return Err(io::const_io_error!(
564-
ErrorKind::WouldBlock,
558+
io::ErrorKind::WouldBlock,
565559
"posix_spawnp returned EBADF too often",
566560
));
567561
}
@@ -729,7 +723,7 @@ impl Command {
729723
// But we cannot obtain its pid even though pidfd_getpid support was verified earlier.
730724
// This might happen if libc can't open procfs because the file descriptor limit has been reached.
731725
libc::close(pidfd);
732-
return Err(Error::new(
726+
return Err(io::Error::new(
733727
e.kind(),
734728
"pidfd_spawnp succeeded but the child's PID could not be obtained",
735729
));
@@ -1190,7 +1184,6 @@ impl ExitStatusError {
11901184
mod linux_child_ext {
11911185

11921186
use crate::os::linux::process as os;
1193-
use crate::sys::pal::unix::ErrorKind;
11941187
use crate::sys::pal::unix::linux::pidfd as imp;
11951188
use crate::sys_common::FromInner;
11961189
use crate::{io, mem};
@@ -1203,7 +1196,9 @@ mod linux_child_ext {
12031196
.as_ref()
12041197
// SAFETY: The os type is a transparent wrapper, therefore we can transmute references
12051198
.map(|fd| unsafe { mem::transmute::<&imp::PidFd, &os::PidFd>(fd) })
1206-
.ok_or_else(|| io::Error::new(ErrorKind::Uncategorized, "No pidfd was created."))
1199+
.ok_or_else(|| {
1200+
io::Error::new(io::ErrorKind::Uncategorized, "No pidfd was created.")
1201+
})
12071202
}
12081203

12091204
fn into_pidfd(mut self) -> Result<os::PidFd, Self> {

library/std/src/sys/pal/unix/process/process_vxworks.rs

+2-6
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,8 @@ impl Command {
2121
use crate::sys::cvt_r;
2222
let envp = self.capture_env();
2323

24-
if self.saw_nul() {
25-
return Err(io::const_io_error!(
26-
ErrorKind::InvalidInput,
27-
"nul byte found in provided data",
28-
));
29-
}
24+
self.validate_input()?;
25+
3026
let (ours, theirs) = self.setup_io(default, needs_stdin)?;
3127
let mut p = Process { pid: 0, status: None };
3228

library/std/src/sys/pal/windows/process.rs

+34-6
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,40 @@ impl AsRef<OsStr> for EnvKey {
142142
}
143143
}
144144

145-
pub(crate) fn ensure_no_nuls<T: AsRef<OsStr>>(str: T) -> io::Result<T> {
146-
if str.as_ref().encode_wide().any(|b| b == 0) {
147-
Err(io::const_io_error!(ErrorKind::InvalidInput, "nul byte found in provided data"))
148-
} else {
149-
Ok(str)
145+
/// Returns an error if the provided string has a NUL byte anywhere or a `=`
146+
/// after the first character.
147+
fn ensure_env_var_name<T: AsRef<OsStr>>(s: T) -> io::Result<T> {
148+
fn inner(s: &OsStr) -> io::Result<()> {
149+
let err = || {
150+
Err(io::const_io_error!(
151+
ErrorKind::InvalidInput,
152+
"nul or '=' byte found in provided data",
153+
))
154+
};
155+
let mut iter = s.as_encoded_bytes().iter();
156+
if iter.next() == Some(&0) {
157+
return err();
158+
}
159+
if iter.any(|&b| b == 0 || b == b'=') {
160+
return err();
161+
}
162+
Ok(())
163+
}
164+
inner(s.as_ref())?;
165+
Ok(s)
166+
}
167+
168+
/// Returns an error if the provided string has a NUL byte anywhere.
169+
pub(crate) fn ensure_no_nuls<T: AsRef<OsStr>>(s: T) -> io::Result<T> {
170+
fn inner(s: &OsStr) -> io::Result<()> {
171+
if s.as_encoded_bytes().iter().any(|&b| b == 0) {
172+
Err(io::const_io_error!(ErrorKind::InvalidInput, "nul byte found in provided data"))
173+
} else {
174+
Ok(())
175+
}
150176
}
177+
inner(s.as_ref())?;
178+
Ok(s)
151179
}
152180

153181
pub struct Command {
@@ -873,7 +901,7 @@ fn make_envp(maybe_env: Option<BTreeMap<EnvKey, OsString>>) -> io::Result<(*mut
873901
}
874902

875903
for (k, v) in env {
876-
ensure_no_nuls(k.os_string)?;
904+
ensure_env_var_name(k.os_string)?;
877905
blk.extend(k.utf16);
878906
blk.push('=' as u16);
879907
blk.extend(ensure_no_nuls(v)?.encode_wide());

0 commit comments

Comments
 (0)