Skip to content

Commit 5bcad63

Browse files
authored
Sudoedit compliance tests (batch 2) (#1216)
2 parents 61f42f4 + 5d2dc28 commit 5bcad63

File tree

4 files changed

+191
-9
lines changed

4 files changed

+191
-9
lines changed

src/sudo/pipeline/edit.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::process::exit;
33
use super::super::cli::SudoEditOptions;
44
use crate::common::{Context, Error};
55
use crate::exec::ExitReason;
6+
use crate::log::{user_error, user_info};
67
use crate::sudoers::Authorization;
78
use crate::system::audit;
89

@@ -34,16 +35,20 @@ pub fn run_edit(edit_opts: SudoEditOptions) -> Result<(), Error> {
3435
&context.target_group,
3536
) {
3637
Ok(file) => opened_files.push((path, file)),
37-
Err(error) => eprintln_ignore_io_error!("error opening {arg}: {error}"),
38+
// ErrorKind::FilesystemLoop was only stabilized in 1.83
39+
Err(error) if error.raw_os_error() == Some(libc::ELOOP) => {
40+
user_error!("{arg}: editing symbolic links is not permitted")
41+
}
42+
Err(error) => user_error!("error opening {arg}: {error}"),
3843
}
3944
} else {
40-
eprintln_ignore_io_error!("invalid path: {arg}");
45+
user_error!("invalid path: {arg}");
4146
}
4247
}
4348

4449
if opened_files.len() != context.files_to_edit.len() {
45-
eprintln_ignore_io_error!("please address the problems and try again");
46-
return Ok(());
50+
user_info!("please address the problems and try again");
51+
return Err(Error::Silent);
4752
}
4853

4954
// run command and return corresponding exit code

src/system/audit.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ use std::os::unix::{
1010
};
1111
use std::path::{Component, Path};
1212

13-
use super::{cerr, inject_group, set_supplementary_groups, Group, GroupId, User, UserId};
13+
use super::{
14+
cerr, inject_group, interface::UnixUser, set_supplementary_groups, Group, GroupId, User, UserId,
15+
};
1416
use crate::common::resolve::CurrentUser;
1517

1618
/// Temporary change privileges --- essentially a 'mini sudo'
@@ -109,6 +111,7 @@ pub fn secure_open_cookie_file(path: impl AsRef<Path>) -> io::Result<File> {
109111
.read(true)
110112
.write(true)
111113
.create(true)
114+
.truncate(false)
112115
.mode(mode(Category::Owner, Op::Write) | mode(Category::Owner, Op::Read));
113116

114117
secure_open_impl(path.as_ref(), &mut open_options, true, true)
@@ -214,7 +217,16 @@ pub fn secure_open_for_sudoedit(
214217
target_group: &Group,
215218
) -> io::Result<File> {
216219
sudo_call(target_user, target_group, || {
217-
traversed_secure_open(path, current_user)
220+
if current_user.is_root() {
221+
OpenOptions::new()
222+
.read(true)
223+
.write(true)
224+
.create(true)
225+
.truncate(false)
226+
.open(path)
227+
} else {
228+
traversed_secure_open(path, current_user)
229+
}
218230
})?
219231
}
220232

@@ -248,7 +260,7 @@ fn traversed_secure_open(path: impl AsRef<Path>, forbidden_user: &User) -> io::R
248260
{
249261
Err(io::Error::new(
250262
ErrorKind::PermissionDenied,
251-
"cannot open a file in a path writeable by the user",
263+
"cannot open a file in a path writable by the user",
252264
))
253265
} else {
254266
Ok(())
@@ -291,13 +303,13 @@ mod test {
291303
assert!(std::fs::File::open("/etc/hosts").is_ok());
292304
assert!(secure_open_sudoers("/etc/hosts", false).is_ok());
293305

294-
// /tmp should be readable, but not secure (writeable by group other than root)
306+
// /tmp should be readable, but not secure (writable by group other than root)
295307
assert!(std::fs::File::open("/tmp").is_ok());
296308
assert!(secure_open_sudoers("/tmp", false).is_err());
297309

298310
#[cfg(target_os = "linux")]
299311
{
300-
// /var/log/wtmp should be readable, but not secure (writeable by group other than root)
312+
// /var/log/wtmp should be readable, but not secure (writable by group other than root)
301313
// It doesn't exist on many non-Linux systems however.
302314
if std::fs::File::open("/var/log/wtmp").is_ok() {
303315
assert!(secure_open_sudoers("/var/log/wtmp", false).is_err());

test-framework/sudo-compliance-tests/src/sudoedit.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use crate::{
66
Result, DEFAULT_EDITOR, GROUPNAME, PANIC_EXIT_CODE, SUDOERS_ALL_ALL_NOPASSWD, USERNAME,
77
};
88

9+
mod limits;
10+
911
const LOGS_PATH: &str = "/tmp/logs.txt";
1012
const CHMOD_EXEC: &str = "555";
1113
const EDITOR_DUMMY: &str = "#!/bin/sh
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
use sudo_test::{Command, Directory, Env, TextFile, ROOT_GROUP};
2+
3+
use crate::{DEFAULT_EDITOR, OTHER_USERNAME, SUDOERS_ALL_ALL_NOPASSWD, USERNAME};
4+
5+
const CHMOD_EXEC: &str = "555";
6+
const EDITOR_DUMMY: &str = "#!/bin/sh
7+
echo \"#\" >> \"$1\"";
8+
9+
#[test]
10+
fn cannot_edit_writable_paths() {
11+
let env = Env(SUDOERS_ALL_ALL_NOPASSWD)
12+
.user(USERNAME)
13+
.directory(Directory("/tmp/bar").chmod("755"))
14+
.file(DEFAULT_EDITOR, TextFile(EDITOR_DUMMY).chmod(CHMOD_EXEC))
15+
.build();
16+
17+
for file in ["/tmp/foo.sh", "/tmp/bar/foo.sh", "/var/tmp/foo.sh"] {
18+
let output = Command::new("sudoedit")
19+
.as_user(USERNAME)
20+
.arg(file)
21+
.output(&env);
22+
23+
output.assert_exit_code(1);
24+
25+
if sudo_test::is_original_sudo() {
26+
if file != "/tmp/bar/foo.sh" {
27+
assert_contains!(
28+
output.stderr(),
29+
"editing files in a writable directory is not permitted"
30+
);
31+
} else {
32+
// I don't know why ogsudo gives this error -- probably because opening failed
33+
assert_contains!(output.stderr(), "No such file or directory");
34+
}
35+
} else {
36+
assert_contains!(
37+
output.stderr(),
38+
"cannot open a file in a path writable by the user"
39+
);
40+
}
41+
}
42+
}
43+
44+
#[test]
45+
fn can_edit_writable_paths_as_root() {
46+
// note: we already have tests that sudoedit "works" so we are skipping
47+
// the content check here: the point here is that sudoedit does not stop
48+
// the user.
49+
50+
let env = Env(SUDOERS_ALL_ALL_NOPASSWD)
51+
.user(USERNAME)
52+
.directory(Directory("/tmp/bar").chmod("755"))
53+
.file(DEFAULT_EDITOR, TextFile(EDITOR_DUMMY).chmod(CHMOD_EXEC))
54+
.build();
55+
56+
let file = "/tmp/foo.sh";
57+
Command::new("sudoedit")
58+
.arg(file)
59+
.output(&env)
60+
.assert_success();
61+
}
62+
63+
#[test]
64+
fn cannot_edit_symlinks() {
65+
let env = Env(SUDOERS_ALL_ALL_NOPASSWD)
66+
.user(USERNAME)
67+
.file(DEFAULT_EDITOR, TextFile(EDITOR_DUMMY).chmod(CHMOD_EXEC))
68+
.build();
69+
70+
let file = "/usr/bin/sudoedit";
71+
72+
let output = Command::new("sudoedit")
73+
.as_user(USERNAME)
74+
.arg(file)
75+
.output(&env);
76+
77+
assert_contains!(output.stderr(), "editing symbolic links is not permitted");
78+
79+
output.assert_exit_code(1);
80+
}
81+
82+
#[test]
83+
fn cannot_edit_files_target_user_cannot_access() {
84+
let file = "/test.txt";
85+
86+
let env = Env(SUDOERS_ALL_ALL_NOPASSWD)
87+
.user(USERNAME)
88+
.user(OTHER_USERNAME)
89+
.group(USERNAME)
90+
.group(OTHER_USERNAME)
91+
.file(DEFAULT_EDITOR, TextFile(EDITOR_DUMMY).chmod(CHMOD_EXEC))
92+
.file(
93+
file,
94+
TextFile("")
95+
.chown(format!("{USERNAME}:{ROOT_GROUP}"))
96+
.chmod("460"),
97+
)
98+
.build();
99+
100+
let test_cases = [
101+
// incorrect user
102+
&["-u", OTHER_USERNAME][..],
103+
// correct user, but does not have write permits
104+
&["-u", USERNAME][..],
105+
// incorrect group
106+
&["-u", OTHER_USERNAME, "-g", USERNAME][..],
107+
// group permission doesn't override matching user permissions
108+
&["-u", USERNAME, "-g", ROOT_GROUP][..],
109+
&["-g", ROOT_GROUP][..],
110+
];
111+
112+
for args in test_cases {
113+
let output = Command::new("sudoedit")
114+
.args(args)
115+
.arg(file)
116+
.as_user(USERNAME)
117+
.output(&env);
118+
119+
assert_contains!(output.stderr(), "Permission denied");
120+
output.assert_exit_code(1);
121+
}
122+
}
123+
124+
#[test]
125+
fn can_edit_files_target_user_or_group_can_access() {
126+
// note: we already have tests that sudoedit "works" so we are skipping
127+
// the content check here: the point here is that sudoedit does not stop
128+
// the user.
129+
130+
let file = "/test.txt";
131+
let env = Env(SUDOERS_ALL_ALL_NOPASSWD)
132+
.user(USERNAME)
133+
.user(OTHER_USERNAME)
134+
.group(OTHER_USERNAME)
135+
.file(DEFAULT_EDITOR, TextFile(EDITOR_DUMMY).chmod(CHMOD_EXEC))
136+
.file(
137+
file,
138+
TextFile("")
139+
.chown(format!("{OTHER_USERNAME}:{OTHER_USERNAME}"))
140+
.chmod("660"),
141+
)
142+
.build();
143+
144+
for user in ["root", OTHER_USERNAME] {
145+
Command::new("sudoedit")
146+
.args(["-u", user, file])
147+
.as_user(USERNAME)
148+
.output(&env)
149+
.assert_success();
150+
}
151+
152+
Command::new("sudoedit")
153+
.args(["-g", OTHER_USERNAME, file])
154+
.as_user(USERNAME)
155+
.output(&env)
156+
.assert_success();
157+
158+
Command::new("sudoedit")
159+
.args(["-u", USERNAME, "-g", OTHER_USERNAME, file])
160+
.as_user(USERNAME)
161+
.output(&env)
162+
.assert_success();
163+
}

0 commit comments

Comments
 (0)