Skip to content

Commit 52d093c

Browse files
authored
Add path resolution for sudoedit (#1193)
2 parents 2b4c0cf + dcb118d commit 52d093c

File tree

4 files changed

+57
-10
lines changed

4 files changed

+57
-10
lines changed

src/common/context.rs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::io;
1+
use std::{env, io};
22

33
use crate::common::{HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1, HARDENED_ENUM_VALUE_2};
44
use crate::exec::{RunOptions, Umask};
@@ -34,6 +34,9 @@ pub struct Context {
3434
pub use_pty: bool,
3535
pub noexec: bool,
3636
pub umask: Umask,
37+
// sudoedit
38+
#[cfg_attr(not(feature = "sudoedit"), allow(unused))]
39+
pub files_to_edit: Vec<Option<SudoPath>>,
3740
}
3841

3942
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
@@ -74,7 +77,7 @@ impl Context {
7477
let path = if let Some(path) = override_path {
7578
path
7679
} else {
77-
system_path = std::env::var("PATH").unwrap_or_default();
80+
system_path = env::var("PATH").unwrap_or_default();
7881
system_path.as_ref()
7982
};
8083

@@ -98,6 +101,7 @@ impl Context {
98101
use_pty: true,
99102
noexec: false,
100103
umask: Umask::Preserve,
104+
files_to_edit: vec![],
101105
})
102106
}
103107

@@ -109,12 +113,35 @@ impl Context {
109113
let (target_user, target_group) =
110114
resolve_target_user_and_group(&sudo_options.user, &sudo_options.group, &current_user)?;
111115

116+
// resolve file arguments; if something can't be resolved, don't add it to the "edit" list
117+
let resolved_args = sudo_options.positional_args.iter().map(|arg| {
118+
crate::common::resolve::canonicalize_newfile(arg)
119+
.map_err(|_| arg)
120+
.and_then(|path| path.into_os_string().into_string().map_err(|_| arg))
121+
});
122+
123+
let files_to_edit = resolved_args
124+
.clone()
125+
.map(|path| path.ok().map(SudoPath::from_cli_string))
126+
.collect();
127+
128+
// if a path resolved to something that isn't in UTF-8, it means it isn't in the sudoers file
129+
// as well and so we treat it "as is" wrt. the policy lookup and fail if the user is allowed
130+
// by the policy to edit that file. this is to prevent leaking information.
131+
let arguments = resolved_args
132+
.map(|arg| match arg {
133+
Ok(arg) => arg,
134+
Err(arg) => arg.to_owned(),
135+
})
136+
.collect();
137+
112138
// TODO: the more Rust way of doing things would be to create an alternative for sudoedit instead;
113139
// but a stringly typed interface feels the most decent thing to do (if we can pull it off)
114-
// since "sudoedit" really is like a builtin command to sudo.
140+
// since "sudoedit" really is like a builtin command to sudo. We may want to be a bit 'better' than
141+
// ogsudo in the future.
115142
let command = CommandAndArguments {
116143
command: std::path::PathBuf::from("sudoedit"),
117-
arguments: sudo_options.positional_args,
144+
arguments,
118145
..Default::default()
119146
};
120147

@@ -135,6 +162,7 @@ impl Context {
135162
use_pty: true,
136163
noexec: false,
137164
umask: Umask::Preserve,
165+
files_to_edit,
138166
})
139167
}
140168
pub fn from_validate_opts(sudo_options: SudoValidateOptions) -> Result<Context, Error> {
@@ -160,6 +188,7 @@ impl Context {
160188
use_pty: true,
161189
noexec: false,
162190
umask: Umask::Preserve,
191+
files_to_edit: vec![],
163192
})
164193
}
165194

@@ -182,7 +211,7 @@ impl Context {
182211
let path = if let Some(path) = override_path {
183212
path
184213
} else {
185-
system_path = std::env::var("PATH").unwrap_or_default();
214+
system_path = env::var("PATH").unwrap_or_default();
186215
system_path.as_ref()
187216
};
188217

@@ -206,6 +235,7 @@ impl Context {
206235
use_pty: true,
207236
noexec: false,
208237
umask: Umask::Preserve,
238+
files_to_edit: vec![],
209239
})
210240
}
211241

src/common/resolve.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,20 @@ mod tests {
329329
/// Resolve symlinks in all the directories leading up to a file, but
330330
/// not the file itself; this allows sudo to specify a precise policy with
331331
/// tools like busybox or pgrep (which is a symlink to pgrep on systems)
332+
/// This function will check for existence.
332333
pub fn canonicalize<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
334+
let reconstructed_path = canonicalize_newfile(path)?;
335+
336+
// access the object to generate the regular error if it does not exist
337+
let _ = fs::metadata(&reconstructed_path)?;
338+
339+
Ok(reconstructed_path)
340+
}
341+
342+
/// Resolve symlinks in all the directories leading up to a file, but
343+
/// not the file itself; this allows us to keep symlinks as is, and will
344+
/// also work on non-existing files.
345+
pub fn canonicalize_newfile<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
333346
let path = path.as_ref();
334347
let Some(parent) = path.parent() else {
335348
// path is "/" or a prefix
@@ -344,9 +357,6 @@ pub fn canonicalize<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
344357
canon_path
345358
};
346359

347-
// access the object to generate the regular error if it does not exist
348-
let _ = fs::metadata(&reconstructed_path)?;
349-
350360
Ok(reconstructed_path)
351361
}
352362

@@ -358,7 +368,7 @@ mod test {
358368
#[test]
359369
fn canonicalization() {
360370
assert_eq!(canonicalize("/").unwrap(), Path::new("/"));
361-
assert_eq!(canonicalize("").unwrap(), Path::new(""));
371+
assert!(canonicalize("").is_err());
362372
if cfg!(any(target_os = "linux", target_os = "macos")) {
363373
// this test REQUIRES /usr/bin/unxz to be a symlink for /usr/bin/xz
364374
assert_eq!(

src/sudo/env/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ fn create_test_context(sudo_options: SudoRunOptions) -> Context {
136136
noexec: false,
137137
bell: false,
138138
umask: Umask::Preserve,
139+
files_to_edit: vec![],
139140
}
140141
}
141142

src/sudo/pipeline/edit.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,19 @@ pub fn run_edit(edit_opts: SudoEditOptions) -> Result<(), Error> {
2323

2424
let pid = context.process.pid;
2525

26+
for (path, arg) in context.files_to_edit.iter().zip(&context.command.arguments) {
27+
if path.is_none() {
28+
eprintln_ignore_io_error!("invalid path: {arg}")
29+
}
30+
}
31+
2632
// run command and return corresponding exit code
2733
let command_exit_reason = {
2834
super::log_command_execution(&context);
2935

3036
eprintln_ignore_io_error!(
3137
"this would launch sudoedit as requested, to edit the files: {:?}",
32-
context.command.arguments.as_slice()
38+
context.files_to_edit.as_slice()
3339
);
3440

3541
Ok::<_, std::io::Error>(ExitReason::Code(42))

0 commit comments

Comments
 (0)