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

发布于 2024-10-11 00:29:50 字数 582 浏览 6 评论 0原文

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

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

这是一个说明我的问题的片段:

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

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

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

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

感谢您的帮助
套利

I'm building a daemon that will help me manage my server(s). Webmin works fine, as does just opening a shell to the server, but I'd prefer to be able to control server operations from a UI I design, and also expose some functionality to end users.

The daemon will pick up actions from a queue and execute them. However, since I'll be accepting input from users, I want to make sure they're not permitted to inject something dangerous into a privileged shell command.

Here's a fragment that exemplifies my problem:

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

A gist that explains more: https://gist.github.com/773292

I'm not positive if typical escaping and sanitizing of inputs is enough for this case, and being a designer, I don't have a ton of security-related experience. I know that this is something that should probably be obvious to me, but its not!

How can I ensure that the web application that will create and serialize the actions can't pass dangerous text into the privileged process that receives the actions?

Thanks for the help
arb

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

懒猫 2024-10-18 00:29:51

看起来您不需要外壳来完成您正在做的事情。请参阅此处的 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 技巧。

It doesn't look like you need a shell for what you're doing. See the documentation for system here: http://ruby-doc.org/core/classes/Kernel.html#M001441

You should use the second form of system. Your example above would become:

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

A nicer (IMO) way to write this is:

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

The arguments this way are passed directly into the execve call, so you don't have to worry about sneaky shell tricks.

旧话新听 2024-10-18 00:29:51

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

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

更多信息请参见:获取 Ruby 中 system() 调用的输出

If you need not just the exit status but also the result you probably want to use Open3.popen3:

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

More information here: Getting output of system() calls in Ruby

忘羡 2024-10-18 00:29:51

我建议研究“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

I'd suggest looking into the 'shellwords' module. This script:

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

outputs the escaped text and the expected output:

echo \'hello\ world\'\;\ \!\%\&\ some\ stuff and\ another\ argument
'hello world'; !%& some stuff and another argument
妄司 2024-10-18 00:29:51

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

尝试系统“dir”、“&”、“echo”、“hi!”
在 Windows 系统上。 dir 和 echo 都将运行。当然,Echo 也可能是一种远没有那么无害的东西。

This is an old question, but since it's pretty much the only real answer you'll find when googling I thought I'd add a caveat. The multi argument version of system seems reasonably safe on Linux, but it is NOT on Windows.

Try system "dir", "&", "echo", "hi!"
on a Windows system. Both dir and echo will be run. Echo could of course just as well be something far less innocuous.

陈独秀 2024-10-18 00:29:51

我知道这是一个旧线程,但还有另一个选项被 Simon Hürlimann 轻轻提及。

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

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

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

我选择将输出格式化为 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 缓冲区时,如果 stdout_buffer.length > 则使用 command_stdout = stdout_buffer 0 来控制是否分配 command_stdout 变量。当不存在数据时,您应该传递 nil 而不是 ""。后面交数据的时候就更清楚了。

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

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

  • 没有元字符
  • 没有 shell 保留字,也没有特殊的内置
  • Ruby 无需 shell 即可直接调用命令

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

这只是防止 Ruby 在您传递格式错误的命令时抛出 'spawn': No such file or directory 错误。相反,它会将其直接传递到内核,错误将在内核中得到妥善解决并显示为 STDERR 而不是未捕获的异常。

I know this is an old thread, but there is another option that was lightly touched on by Simon Hürlimann.

There is not a lot of information about this topic and I think this might help others in need.

For this example we'll use Open3 which gives you the ability to run commands synchronously or asynchronously, and provides stdout, stderr, exit codes, and PID.

Open3 grants you access to stdout, stderr, exit codes and a thread to wait for the child process when running another program. You can specify various attributes, redirections, current directory, etc., of the program in the same way as for Process.spawn. (Source: Open3 Docs)

I chose to format the output as a CommandStatus object. This contains our stdout, stderr, pid (Of the worker thread) and 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}"

While reading the STDOUT/ERR buffers, I use command_stdout = stdout_buffer if stdout_buffer.length > 0 to control whether the command_stdout variable is assigned or not. You should pass nil instead of "" when no data is present. It's more clear when handing data later on.

You probably noticed me using command + ';'. The reason for this is based on the documentation from Kernel.exec (Which is what popen3 uses):

If the string from the first form (exec("command")) follows these
simple rules:

  • no meta characters
  • no shell reserved word and no special built-in
  • Ruby invokes the command directly without shell

You can force shell invocation by adding ";" to the string (because
";" is a meta character)

This simply prevents a Ruby from throwing a 'spawn': No such file or directory error if you pass a malformed command. Instead it will pass it straight to the kernel where the error will be resolved gracefully and appear as STDERR instead of an uncaught exception.

白鸥掠海 2024-10-18 00:29:51

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

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

#read 将在返回之前关闭 IO)

Modern, secure and simple solution (popen will escape arguments for you):

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

(#read will close the IO before returning)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文