在Ruby中形成卫生shell命令或系统调用

时间:2011-01-10 19:28:06

标签: ruby security shell system sanitization

我正在构建一个可以帮助我管理服务器的守护进程。 Webmin运行良好,就像打开服务器的shell一样,但我更喜欢能够从我设计的UI控制服务器操作,并向最终用户公开一些功能。

守护程序将从队列中获取操作并执行它们。但是,由于我将接受用户的输入,我想确保他们不被允许在特权shell命令中注入危险的东西。

这是一个例证我问题的片段:

def perform
  system "usermod -p #{@options['shadow']} #{@options['username']}"
end

解释更多内容的要点:https://gist.github.com/773292

如果输入的典型逃避和消毒对于这种情况来说已经足够了,我并不乐观,作为一名设计师,我没有大量与安全相关的经验。 我知道这对我来说应该是显而易见的,但不是!

如何确保将创建和序列化操作的Web应用程序无法将危险文本传递到接收操作的特权进程中?

感谢您的帮助 ARB

6 个答案:

答案 0 :(得分:19)

看起来你需要一个shell来做你正在做的事情。请参阅system此处的文档:http://ruby-doc.org/core/classes/Kernel.html#M001441

您应该使用system的第二种形式。上面的例子将成为:

system 'usermod', '-p', @options['shadow'], @options['username']

更好(IMO)的方式来写这个:

system *%W(usermod -p #{@options['shadow']} #{@options['username']})

这种方式的参数直接传递给execve调用,所以你不必担心偷偷摸摸的shell技巧。

答案 1 :(得分:16)

如果您不仅需要退出状态,还需要结果,您可能需要使用Open3.popen3

require 'open3'
stdin, stdout, stderr = Open3.popen3('usermod', '-p', @options['shadow'], @options['username'])
stdout.gets
sterr.gets

此处提供更多信息:Getting output of system() calls in Ruby

答案 2 :(得分:2)

我建议查看'shellwords'模块。这个脚本:

require 'shellwords'
parts = ['echo', "'hello world'; !%& some stuff", 'and another argument']
command = Shellwords.shelljoin( parts )
puts command
output = `#{ command }`
puts output

输出转义文本和预期输出:

echo \'hello\ world\'\;\ \!\%\&\ some\ stuff and\ another\ argument
'hello world'; !%& some stuff and another argument

答案 3 :(得分:1)

这是一个老问题,但因为它几乎是你在谷歌搜索时会发现的唯一真正的答案,我想我会添加一个警告。系统的多参数版本似乎在Linux上相当安全,但它不在Windows上。

试试system "dir", "&", "echo", "hi!" 在Windows系统上。 dir和echo都会运行。 Echo当然也可以是一种无害的东西。

答案 4 :(得分:0)

我知道这是一个老话题,但SimonHürlimann轻轻触及了另一个选项。

关于这个主题的信息不多,我认为这可能会帮助其他有需要的人。

对于此示例,我们将使用Open3,这使您能够同步或异步运行命令,并提供标准输出标准退出代码 PID

Open3允许您访问stdout,stderr,退出代码和一个线程,以便在运行其他程序时等待子进程。您可以使用与Process.spawn相同的方式指定程序的各种属性,重定向,当前目录等。 (来源:Open3 Docs

我选择将输出格式化为CommandStatus对象。这包含我们的stdoutstderrpid(工作线程)和exitstatus

class Command
  require 'open3'

  class CommandStatus
    @stdout     = nil
    @stderr     = nil
    @pid        = nil
    @exitstatus = nil

    def initialize(stdout, stderr, process)
      @stdout     = stdout
      @stderr     = stderr
      @pid        = process.pid
      @exitstatus = process.exitstatus
    end

    def stdout
      @stdout
    end

    def stderr
      @stderr
    end

    def exit_status
      @exitstatus
    end

    def pid
      @pid
    end
  end

  def self.execute(command)
    command_stdout = nil
    command_stderr = nil
    process = Open3.popen3(ENV, command + ';') do |stdin, stdout, stderr, thread|
      stdin.close
      stdout_buffer   = stdout.read
      stderr_buffer   = stderr.read
      command_stdout  = stdout_buffer if stdout_buffer.length > 0
      command_stderr  = stderr_buffer if stderr_buffer.length > 0
      thread.value # Wait for Process::Status object to be returned
    end
    return CommandStatus.new(command_stdout, command_stderr, process)
  end
end


cmd = Command::execute("echo {1..10}")

puts "STDOUT: #{cmd.stdout}"
puts "STDERR: #{cmd.stderr}"
puts "EXIT: #{cmd.exit_status}"

在读取STDOUT / ERR缓冲区时,我使用command_stdout = stdout_buffer if stdout_buffer.length > 0来控制是否分配了command_stdout变量。如果没有数据,您应该通过nil而不是""。稍后处理数据时会更清楚。

你可能注意到我使用了command + ';'。原因是基于Kernel.exec的文档(这是popen3使用的):

  

如果第一种形式的字符串(exec(“command”))遵循这些   简单的规则:

     
      
  • 没有元字符
  •   
  • 没有shell保留字,没有特殊的内置
  •   
  • Ruby直接调用命令而不使用shell
  •   
     

您可以通过添加“;”来强制进行shell调用到字符串(因为   “;”是一个元字符)

如果传递格式错误的命令,这只会阻止Ruby抛出'spawn': No such file or directory错误。相反,它会将它直接传递给内核,在那里优雅地解决错误并显示为STDERR而不是未捕获的异常。

答案 5 :(得分:0)

现代、安全且简单的解决方案(popen 将为您转义):

IO.popen(['usermod', '-p', @options['shadow'], @options['username']]).read

#read 会在返回前关闭 IO)