Skip to main content

evalbox_sandbox/
validate.rs

1//! Input validation for sandbox execution.
2//!
3//! Validates user input before sandbox execution to prevent:
4//!
5//! - **Empty commands** - Would cause exec to fail
6//! - **Null bytes** - Could cause string truncation attacks
7//! - **Path traversal** - `../` could escape workspace
8//! - **Absolute paths** - Could reference host filesystem
9//!
10//! ## Example
11//!
12//! ```ignore
13//! use evalbox_sandbox::validate::{validate_cmd, validate_path};
14//!
15//! // Valid inputs
16//! assert!(validate_cmd(&["echo", "hello"]).is_ok());
17//! assert!(validate_path("main.py").is_ok());
18//!
19//! // Invalid inputs
20//! assert!(validate_cmd(&[]).is_err());           // Empty command
21//! assert!(validate_path("../etc/passwd").is_err()); // Path traversal
22//! assert!(validate_path("/etc/passwd").is_err());   // Absolute path
23//! ```
24
25use std::path::Path;
26
27use thiserror::Error;
28
29/// Validation error for sandbox inputs.
30#[derive(Debug, Clone, PartialEq, Eq, Error)]
31pub enum ValidationError {
32    #[error("command cannot be empty")]
33    EmptyCommand,
34
35    #[error("argument {0} is empty")]
36    EmptyArgument(usize),
37
38    #[error("null byte in input")]
39    NullByte,
40
41    #[error("path traversal not allowed")]
42    PathTraversal,
43
44    #[error("absolute path not allowed")]
45    AbsolutePath,
46
47    #[error("path cannot be empty")]
48    EmptyPath,
49}
50
51/// Validate command and arguments.
52pub fn validate_cmd(cmd: &[&str]) -> Result<(), ValidationError> {
53    if cmd.is_empty() {
54        return Err(ValidationError::EmptyCommand);
55    }
56    for (i, arg) in cmd.iter().enumerate() {
57        if arg.is_empty() {
58            return Err(ValidationError::EmptyArgument(i));
59        }
60        if arg.contains('\0') {
61            return Err(ValidationError::NullByte);
62        }
63    }
64    Ok(())
65}
66
67/// Validate a relative path (no `..`, no absolute).
68pub fn validate_path(path: &str) -> Result<(), ValidationError> {
69    if path.is_empty() {
70        return Err(ValidationError::EmptyPath);
71    }
72    if path.contains('\0') {
73        return Err(ValidationError::NullByte);
74    }
75    if path.starts_with('/') {
76        return Err(ValidationError::AbsolutePath);
77    }
78    if has_traversal(path) {
79        return Err(ValidationError::PathTraversal);
80    }
81    Ok(())
82}
83
84fn has_traversal(path: &str) -> bool {
85    Path::new(path)
86        .components()
87        .any(|c| matches!(c, std::path::Component::ParentDir))
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn cmd_valid() {
96        assert!(validate_cmd(&["echo", "hello"]).is_ok());
97    }
98
99    #[test]
100    fn cmd_empty() {
101        assert_eq!(validate_cmd(&[]), Err(ValidationError::EmptyCommand));
102    }
103
104    #[test]
105    fn path_traversal() {
106        assert_eq!(
107            validate_path("../etc/passwd"),
108            Err(ValidationError::PathTraversal)
109        );
110    }
111
112    #[test]
113    fn path_absolute() {
114        assert_eq!(
115            validate_path("/etc/passwd"),
116            Err(ValidationError::AbsolutePath)
117        );
118    }
119}