【问题标题】:Forming sanitary shell commands or system calls in Ruby在 Ruby 中形成卫生 shell 命令或系统调用
【发布时间】:2011-06-06 17:50:51
【问题描述】:

我正在构建一个守护进程来帮助我管理我的服务器。 Webmin 可以正常工作,就像打开服务器的外壳一样,但我更希望能够从我设计的 UI 控制服务器操作,并向最终用户公开一些功能。

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

这里有一个片段可以说明我的问题:

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

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

如果典型的输入转义和清理足以应对这种情况,我并不肯定,而且作为一名设计师,我没有大量与安全相关的经验。 我知道这对我来说应该是显而易见的,但事实并非如此!

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

感谢您的帮助
套利

【问题讨论】:

    标签: ruby security shell system sanitization


    【解决方案1】:

    看起来你不需要一个外壳来做你正在做的事情。在此处查看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技巧。

    【讨论】:

    • 所以,假设我公开了这个类,完全未经过滤(我不会)给最终用户,他们提供了一个影子哈希,然后“用户名;rm -rf /”作为用户名— 这不会产生消除 / 的效果
    • 正确。参数直接传递给执行的程序。您可以自己验证这一点。尝试运行ruby -e 'system *W(ls -l foo; rm -rf /)'
    • 啊,太好了。这确实很有意义。我认为我有这样的想法,即确保应用程序安全自然比实际更难,比简单的步骤和事实更难,好像所有事情都存在危险的边缘情况。这可能是因为我从来没有读过/学到很多。
    • 这还具有使用系统调用的额外好处,而不是通过 bash 进行调用,然后再调用系统(在清理之上节省了一点开销)
    • 上面给出的例子应该改为ruby -e 'system *%w(ls -l foo; rm -rf /)'
    【解决方案2】:

    如果您不仅需要退出状态,还需要您可能想要使用的结果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

    【讨论】:

      【解决方案3】:

      我建议查看“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
      

      【讨论】:

        【解决方案4】:

        这是一个老问题,但由于它几乎是您在谷歌搜索时会找到的唯一真正答案,我想我会添加一个警告。 system 的多参数版本在 Linux 上似乎相当安全,但在 Windows 上却不是。

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

        【讨论】:

          【解决方案5】:

          我知道这是一个旧线程,但是 Simon Hürlimann 轻描淡写地提到了另一个选项。

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

          对于本示例,我们将使用 Open3,它使您能够同步或异步运行命令,并提供 stdoutstderr退出代码PID

          Open3 允许您访问标准输出、标准错误、退出代码和一个线程以在运行另一个程序时等待子进程。您可以使用与 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 而不是未捕获的异常。

          【讨论】:

            【解决方案6】:

            现代、安全和简单的解决方案(popen 将为您避免争论):

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

            #read会在返回前关闭IO)

            【讨论】:

              猜你喜欢
              • 2011-11-03
              • 2013-04-04
              • 2015-08-29
              • 2018-09-18
              • 1970-01-01
              • 1970-01-01
              • 2013-01-06
              • 2010-09-05
              相关资源
              最近更新 更多