@@ -285,3 +285,154 @@ fn getchar(thread: &mut Thread) -> Value
285285 None | Some ( Err ( _) ) => Value :: from ( -1 as i64 ) ,
286286 }
287287}
288+
289+ /// Do some basic safety checking (sandboxing) to minimize
290+ /// security risks for file accesses
291+ fn is_safe_path ( file_path : & str ) -> bool
292+ {
293+ use std:: path:: PathBuf ;
294+ use std:: fs:: canonicalize;
295+
296+ let file_path = file_path. trim ( ) ;
297+ let mut file_path = PathBuf :: from ( file_path) ;
298+
299+ // Reject extensions associated with executable, script or
300+ // loadable library files. The comparison is case-insensitive
301+ // because some filesystems (e.g. macOS, Windows) treat "EXE"
302+ // and "exe" as referring to the same file, so a case-sensitive
303+ // check would be trivially bypassable.
304+ if let Some ( ext) = file_path. extension ( ) {
305+ match ext. to_string_lossy ( ) . to_lowercase ( ) . as_str ( ) {
306+ // Windows executables and scripts
307+ "exe" | "com" | "scr" | "msi" | "cpl" | "dll" |
308+ "bat" | "cmd" | "ps1" | "psm1" | "vbs" | "vbe" |
309+ "js" | "jse" | "wsf" | "wsh" | "hta" | "jar" |
310+ // Unix/macOS executables, libraries and shell scripts
311+ "sh" | "bash" | "zsh" | "csh" | "ksh" | "fish" |
312+ "command" | "so" | "dylib" | "app" | "out" |
313+ // Interpreted language sources
314+ "py" | "pyc" | "pyo" | "rb" | "pl" | "php" | "lua"
315+ => return false ,
316+ _ => { }
317+ }
318+ }
319+
320+ // If this is a file that does not exist yet, pop the trailing
321+ // components from the path. This is necessary for the canonicalize
322+ // function to work
323+ while !file_path. exists ( ) {
324+ file_path. pop ( ) ;
325+
326+ if file_path. as_os_str ( ) . is_empty ( ) {
327+ file_path = PathBuf :: from ( "." ) ;
328+ }
329+ }
330+
331+ // Get the absolute path for the file, resolving symlinks
332+ let file_path = canonicalize ( & file_path) . unwrap ( ) ;
333+ //println!("Canonical path: {:?}", file_path);
334+
335+ // Don't allow access to the current executable
336+ let current_exe = std:: env:: current_exe ( ) . unwrap ( ) ;
337+ let current_exe = canonicalize ( & current_exe) . unwrap ( ) ;
338+ if file_path == current_exe {
339+ println ! ( "file path is current exe" ) ;
340+ return false ;
341+ }
342+
343+ // On Unix/Linux platforms, deny access to files marked as executable
344+ #[ cfg( unix) ]
345+ if file_path. exists ( ) && !file_path. is_dir ( ) {
346+ use std:: os:: unix:: fs:: PermissionsExt ;
347+ let metadata = std:: fs:: metadata ( & file_path) . unwrap ( ) ;
348+ let permissions = metadata. permissions ( ) ;
349+ let mode = permissions. mode ( ) ;
350+ if ( mode & 0o111 ) != 0 {
351+ println ! ( "mode is executable" ) ;
352+ return false ;
353+ }
354+ }
355+
356+ // Get the current working directory
357+ let cwd = std:: env:: current_dir ( ) . unwrap ( ) ;
358+ let cwd = canonicalize ( & cwd) . unwrap ( ) ;
359+ //println!("Canonical cwd: {:?}", cwd);
360+
361+ // If the file path is inside the current working directory, allow access
362+ if file_path. starts_with ( cwd) {
363+ return true ;
364+ }
365+
366+ /*
367+ // Parse the rest arguments
368+ let rest_args = crate::parse_args(std::env::args().collect()).rest;
369+
370+ // For each rest argument supplied on the command-line
371+ for arg in rest_args {
372+
373+ let arg_path = PathBuf::from(arg);
374+
375+ // If this is not a valid path, ignore it
376+ if !arg_path.exists() {
377+ continue;
378+ }
379+
380+ let arg_path = canonicalize(&arg_path).unwrap();
381+
382+ // We can allow access to files in directories
383+ // explicitly specified on the command-line
384+ if arg_path.is_dir() {
385+ if file_path.starts_with(&arg_path) {
386+ return true;
387+ }
388+ }
389+
390+ // We can allow access to files explicitly
391+ // specified on the command-line
392+ if arg_path.is_file() && file_path == arg_path {
393+ return true;
394+ }
395+ }
396+ */
397+
398+ false
399+ }
400+
401+ #[ cfg( test) ]
402+ mod tests
403+ {
404+ use crate :: host:: is_safe_path;
405+
406+ #[ test]
407+ fn safe_path ( )
408+ {
409+ assert ! ( !is_safe_path( "/" ) ) ;
410+ assert ! ( !is_safe_path( "/root" ) ) ;
411+ assert ! ( !is_safe_path( "/usr/bin" ) ) ;
412+ assert ! ( !is_safe_path( "/home/user" ) ) ;
413+ assert ! ( !is_safe_path( ".." ) ) ;
414+ assert ! ( !is_safe_path( "run_me.sh" ) ) ;
415+ assert ! ( !is_safe_path( "run_me.exe" ) ) ;
416+
417+ // Other executable/script/library extensions are unsafe
418+ assert ! ( !is_safe_path( "lib.dylib" ) ) ;
419+ assert ! ( !is_safe_path( "script.py" ) ) ;
420+ assert ! ( !is_safe_path( "app.jar" ) ) ;
421+
422+ // The blocklist must not be bypassable by changing the case
423+ assert ! ( !is_safe_path( "run_me.SH" ) ) ;
424+ assert ! ( !is_safe_path( "MALWARE.Exe" ) ) ;
425+
426+ // Home directory access is not safe
427+ if let Some ( home_path) = std:: env:: home_dir ( ) {
428+ let home_path = home_path. to_str ( ) . unwrap ( ) ;
429+ assert ! ( !is_safe_path( home_path) ) ;
430+ }
431+
432+ // Safe paths inside CWD
433+ assert ! ( is_safe_path( "." ) ) ;
434+ assert ! ( is_safe_path( "foo.txt" ) ) ;
435+ assert ! ( is_safe_path( "data.csv" ) ) ;
436+ assert ! ( is_safe_path( "docs/language.md" ) ) ;
437+ }
438+ }
0 commit comments