1+ use std:: ffi:: { CStr , CString } ;
12use std:: fs:: { DirBuilder , File , Metadata , OpenOptions } ;
23use 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:: { AsRawFd , FromRawFd } ;
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
815enum 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
2331pub 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
2939pub 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 : & File , file_name : & CStr , create : bool ) -> io:: Result < File > {
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 ( File :: from_raw_fd ( fd) )
139+ }
140+ }
141+
142+ #[ cfg_attr( not( feature = "sudoedit" ) , allow( dead_code) ) ]
143+ /// This opens a file making sure that
144+ /// - no directory leading up to the file is editable by the user
145+ /// - no components are a symbolic link
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, & dir, false ) ?;
195+ user_cannot_write ( & cur) ?;
196+ }
197+
198+ cur = open_at ( & cur, & CString :: new ( file_name. as_bytes ( ) ) ?, true ) ?;
199+ user_cannot_write ( & cur) ?;
200+
201+ Ok ( cur)
202+ }
203+
106204#[ cfg( test) ]
107205mod 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