Skip to content

Commit 3f25c84

Browse files
committed
Add basic file access sandboxing function
1 parent db3abd3 commit 3f25c84

1 file changed

Lines changed: 151 additions & 0 deletions

File tree

vm/src/host.rs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)