Skip to content

Commit 06cd294

Browse files
authored
Secure open function for sudoedit (#1200)
2 parents f9f6476 + 57826a3 commit 06cd294

File tree

1 file changed

+147
-3
lines changed

1 file changed

+147
-3
lines changed

src/system/audit.rs

Lines changed: 147 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
use std::ffi::{CStr, CString};
12
use std::fs::{DirBuilder, File, Metadata, OpenOptions};
23
use std::io::{self, Error, ErrorKind};
3-
use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt};
4-
use std::os::unix::prelude::OpenOptionsExt;
5-
use std::path::Path;
4+
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
5+
use std::os::unix::{
6+
ffi::OsStrExt,
7+
fs::{DirBuilderExt, MetadataExt, PermissionsExt},
8+
prelude::OpenOptionsExt,
9+
};
10+
use std::path::{Component, Path};
11+
12+
use super::{cerr, User};
613

714
// of course we can also write "file & 0o040 != 0", but this makes the intent explicit
815
enum Op {
@@ -20,19 +27,23 @@ fn mode(who: Category, what: Op) -> u32 {
2027
(what as u32) << (3 * who as u32)
2128
}
2229

30+
/// Open sudo configuration using various security checks
2331
pub fn secure_open(path: impl AsRef<Path>, check_parent_dir: bool) -> io::Result<File> {
2432
let mut open_options = OpenOptions::new();
2533
open_options.read(true);
34+
2635
secure_open_impl(path.as_ref(), &mut open_options, check_parent_dir, false)
2736
}
2837

38+
/// Open a timestamp cookie file using various security checks
2939
pub fn secure_open_cookie_file(path: impl AsRef<Path>) -> io::Result<File> {
3040
let mut open_options = OpenOptions::new();
3141
open_options
3242
.read(true)
3343
.write(true)
3444
.create(true)
3545
.mode(mode(Category::Owner, Op::Write) | mode(Category::Owner, Op::Read));
46+
3647
secure_open_impl(path.as_ref(), &mut open_options, true, true)
3748
}
3849

@@ -103,6 +114,93 @@ fn secure_open_impl(
103114
Ok(file)
104115
}
105116

117+
#[cfg_attr(not(feature = "sudoedit"), allow(dead_code))]
118+
fn open_at(parent: BorrowedFd, file_name: &CStr, create: bool) -> io::Result<OwnedFd> {
119+
let flags = if create {
120+
libc::O_NOFOLLOW | libc::O_RDWR | libc::O_CREAT
121+
} else {
122+
libc::O_NOFOLLOW | libc::O_RDONLY
123+
};
124+
125+
// the mode for files that are created is hardcoded, as it is in ogsudo
126+
let mode = libc::S_IRUSR | libc::S_IWUSR | libc::S_IRGRP | libc::S_IROTH;
127+
128+
// SAFETY: by design, a correct CStr pointer is passed to openat; only if this call succeeds
129+
// is the file descriptor it returns (which is then necessarily valid) passed to from_raw_fd
130+
unsafe {
131+
let fd = cerr(libc::openat(
132+
parent.as_raw_fd(),
133+
file_name.as_ptr(),
134+
flags,
135+
mode,
136+
))?;
137+
138+
Ok(OwnedFd::from_raw_fd(fd))
139+
}
140+
}
141+
142+
/// This opens a file making sure that
143+
/// - no directory leading up to the file is editable by the user
144+
/// - no components are a symbolic link
145+
#[cfg_attr(not(feature = "sudoedit"), allow(dead_code))]
146+
fn traversed_secure_open(path: impl AsRef<Path>, user: &User) -> io::Result<File> {
147+
let path = path.as_ref();
148+
149+
let Some(file_name) = path.file_name() else {
150+
return Err(io::Error::new(ErrorKind::InvalidInput, "invalid path"));
151+
};
152+
153+
let mut components = path.parent().unwrap_or(Path::new("")).components();
154+
if components.next() != Some(Component::RootDir) {
155+
return Err(io::Error::new(
156+
ErrorKind::InvalidInput,
157+
"path must be absolute",
158+
));
159+
}
160+
161+
let user_cannot_write = |file: &File| -> io::Result<()> {
162+
let meta = file.metadata()?;
163+
let perms = meta.permissions().mode();
164+
165+
if perms & mode(Category::World, Op::Write) != 0
166+
|| (perms & mode(Category::Group, Op::Write) != 0) && user.gid.inner() == meta.gid()
167+
|| (perms & mode(Category::Owner, Op::Write) != 0) && user.uid.inner() == meta.uid()
168+
{
169+
Err(io::Error::new(
170+
ErrorKind::PermissionDenied,
171+
"cannot open a file in a path writeable by the user",
172+
))
173+
} else {
174+
Ok(())
175+
}
176+
};
177+
178+
let mut cur = File::open("/")?;
179+
user_cannot_write(&cur)?;
180+
181+
for component in components {
182+
let dir: CString = match component {
183+
Component::Normal(dir) => CString::new(dir.as_bytes())?,
184+
Component::CurDir => cstr!(".").to_owned(),
185+
Component::ParentDir => cstr!("..").to_owned(),
186+
_ => {
187+
return Err(io::Error::new(
188+
ErrorKind::InvalidInput,
189+
"error in provided path",
190+
))
191+
}
192+
};
193+
194+
cur = open_at(cur.as_fd(), &dir, false)?.into();
195+
user_cannot_write(&cur)?;
196+
}
197+
198+
cur = open_at(cur.as_fd(), &CString::new(file_name.as_bytes())?, true)?.into();
199+
user_cannot_write(&cur)?;
200+
201+
Ok(cur)
202+
}
203+
106204
#[cfg(test)]
107205
mod test {
108206
use super::*;
@@ -135,4 +233,50 @@ mod test {
135233
fn test_secure_open_cookie_file() {
136234
assert!(secure_open_cookie_file("/etc/hosts").is_err());
137235
}
236+
237+
#[test]
238+
fn test_traverse_secure_open_negative() {
239+
use crate::common::resolve::CurrentUser;
240+
241+
let root = User::from_name(cstr!("root")).unwrap().unwrap();
242+
let user = CurrentUser::resolve().unwrap();
243+
244+
// not allowed -- invalid
245+
assert!(traversed_secure_open("/", &root).is_err());
246+
// not allowed since the path is not absolute
247+
assert!(traversed_secure_open("./hello.txt", &root).is_err());
248+
// not allowed since root can write to "/"
249+
assert!(traversed_secure_open("/hello.txt", &root).is_err());
250+
// not allowed since "/tmp" is a directory
251+
assert!(traversed_secure_open("/tmp", &user).is_err());
252+
// not allowed since anybody can write to "/tmp"
253+
assert!(traversed_secure_open("/tmp/foo/hello.txt", &user).is_err());
254+
// not allowed since "/bin" is a symlink
255+
assert!(traversed_secure_open("/bin/hello.txt", &user).is_err());
256+
}
257+
258+
#[test]
259+
fn test_traverse_secure_open_positive() {
260+
use crate::common::resolve::CurrentUser;
261+
use crate::system::{GroupId, UserId};
262+
263+
let other_user = CurrentUser::fake(User {
264+
uid: UserId::new(1042),
265+
gid: GroupId::new(1042),
266+
267+
name: "test".into(),
268+
home: "/home/test".into(),
269+
shell: "/bin/sh".into(),
270+
groups: vec![],
271+
});
272+
273+
// allowed!
274+
let path = std::env::current_dir()
275+
.unwrap()
276+
.join("sudo-rs-test-file.txt");
277+
let file = traversed_secure_open(&path, &other_user).unwrap();
278+
if file.metadata().is_ok_and(|meta| meta.len() == 0) {
279+
std::fs::remove_file(path).unwrap();
280+
}
281+
}
138282
}

0 commit comments

Comments
 (0)