Skip to content

feat: Shell Command Sandbox - Whitelist-Based Execution #67

Description

@amosgeek

Shell Command Sandbox

Overview

Replace the current command blocklist with a secure whitelist-based command execution system with argument validation.

Problem Statement

Current shell security is insufficient:

// Current: Simple blocklist - easily bypassed
fn is_dangerous_command(cmd: &str) -> bool {
    let dangerous_patterns = ["rm -rf /", "mkfs", "dd if=/dev/zero", ...];
    dangerous_patterns.iter().any(|p| cmd.contains(p))
}

Bypass Examples:

  • rm -rf /home (not in blocklist)
  • rmdir -rf / (different command)
  • /bin/rm -rf / (full path)
  • echo 'rm -rf /' | sh (indirect execution)
  • $(rm -rf /) (command substitution)

Proposed Solution

Whitelist-Based Command Execution

// core/src/sandbox/command.rs
pub struct CommandValidator {
    allowed_commands: HashMap<String, CommandPolicy>,
    default_policy: CommandPolicy,
}

pub struct CommandPolicy {
    pub command: String,
    pub allowed_args: Vec<AllowedArg>,
    pub denied_arg_patterns: Vec<Regex>,
    pub requires_permission: PermissionLevel,
    pub timeout_seconds: u64,
}

pub struct AllowedArg {
    pub name: String,
    pub values: ArgValuePolicy,
}

pub enum ArgValuePolicy {
    Any,                          // Any value allowed
    Enum(Vec<String>),           // Specific values only
    Pattern(Regex),              // Must match pattern
    Path(PathPolicy),            // Path validation
}

Command Parsing and Validation

impl CommandValidator {
    pub fn validate(&self, command: &str, user: &UserContext) -> Result<ValidatedCommand, CommandError> {
        // 1. Parse command safely
        let parsed = self.parse_command(command)?;
        
        // 2. Check if command is allowed
        let policy = self.allowed_commands
            .get(&parsed.command)
            .ok_or(CommandError::CommandNotAllowed(parsed.command))?;
        
        // 3. Check user permission
        if !user.permission_level.can_execute(&policy.requires_permission) {
            return Err(CommandError::InsufficientPermission);
        }
        
        // 4. Validate each argument
        for arg in &parsed.args {
            self.validate_argument(arg, policy)?;
        }
        
        // 5. Check for shell metacharacters (prevent injection)
        if self.contains_shell_metacharacters(command) {
            return Err(CommandError::ShellInjectionDetected);
        }
        
        Ok(ValidatedCommand {
            command: parsed.command,
            args: parsed.args,
            timeout: policy.timeout_seconds,
        })
    }
    
    fn contains_shell_metacharacters(&self, cmd: &str) -> bool {
        // Block: |, $(), ``, ;, &&, ||, >, <, 
        // Allow simple commands only
        let dangerous = ['|', '$', '`', ';', '&', '>', '<', '\n'];
        cmd.chars().any(|c| dangerous.contains(&c))
    }
}

Default Allowed Commands

{
  "allowed_commands": {
    "git": {
      "allowed_args": ["clone", "pull", "push", "commit", "status", "log", "diff", "branch", "checkout"],
      "denied_arg_patterns": ["--exec.*"],
      "requires_permission": "member"
    },
    "npm": {
      "allowed_args": ["install", "run", "test", "build"],
      "requires_permission": "member"
    },
    "cargo": {
      "allowed_args": ["build", "test", "run", "check", "clippy"],
      "requires_permission": "member"
    },
    "python": {
      "allowed_args": ["-m", "-c"],
      "denied_arg_patterns": ["import os", "import subprocess", "exec", "eval"],
      "requires_permission": "admin"
    },
    "ls": {
      "allowed_args": ["*"],
      "requires_permission": "guest"
    },
    "cat": {
      "allowed_args": ["*"],
      "requires_permission": "guest"
    }
  }
}

Safe Execution Wrapper

// core/src/tools/shell.rs
impl ShellTool {
    pub async fn execute(&self, command: &str, user: &UserContext) -> Result<ShellResult> {
        // 1. Validate command
        let validated = self.command_validator.validate(command, user)?;
        
        // 2. Execute in controlled environment
        let output = Command::new(&validated.command)
            .args(&validated.args)
            .current_dir(&self.workspace_path)  // Confined to workspace
            .env_clear()  // Clear environment
            .envs(&self.safe_env_vars)  // Only safe env vars
            .output()
            .timeout(Duration::from_secs(validated.timeout))
            .await?;
        
        // 3. Truncate output if needed
        let stdout = self.truncate_output(&output.stdout);
        let stderr = self.truncate_output(&output.stderr);
        
        Ok(ShellResult { stdout, stderr, exit_code: output.status.code() })
    }
}

Implementation Tasks

  • Implement CommandValidator struct
  • Safe command parsing (no shell interpretation)
  • Whitelist-based command checking
  • Argument validation system
  • Shell metacharacter detection
  • Environment variable sanitization
  • Working directory confinement
  • Output truncation
  • Configuration loading

Security Test Cases

#[test]
fn test_command_injection_prevention() {
    let validator = CommandValidator::default();
    
    // Shell injection attempts
    assert!(validator.validate("ls; rm -rf /", &user).is_err());
    assert!(validator.validate("ls && rm -rf /", &user).is_err());
    assert!(validator.validate("ls | cat /etc/passwd", &user).is_err());
    assert!(validator.validate("$(rm -rf /)", &user).is_err());
    assert!(validator.validate("`rm -rf /`", &user).is_err());
    assert!(validator.validate("ls > /tmp/out", &user).is_err());
}

#[test]
fn test_whitelist_enforcement() {
    let validator = CommandValidator::default();
    
    // Only whitelisted commands
    assert!(validator.validate("rm -rf /", &user).is_err());
    assert!(validator.validate("sudo bash", &user).is_err());
    assert!(validator.validate("chmod 777 /", &user).is_err());
    
    // Allowed commands work
    assert!(validator.validate("git status", &user).is_ok());
    assert!(validator.validate("npm install", &user).is_ok());
}

Acceptance Criteria

  • Only whitelisted commands can execute
  • Shell injection is impossible
  • Arguments are validated
  • Execution is confined to workspace
  • Environment is sanitized
  • Output is truncated
  • Clear error messages for security violations
  • All security tests pass

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions