Формирование команд санитарной оболочки или системных вызовов в Ruby

Я создаю демона, который поможет мне управлять моим сервером. Webmin отлично работает, так же как и открытие оболочки на сервере, но я предпочел бы иметь возможность контролировать операции с сервером UI I, а также предоставлять некоторые функции конечным пользователям.

Демон выберет действия из очереди и выполнит их. Однако, поскольку я буду принимать входные данные от пользователей, я хочу убедиться, что им не разрешено вводить что-то опасное в привилегированную команду оболочки.

Вот фрагмент, который иллюстрирует мою проблему:

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

Суть, которая объясняет больше: https://gist.github.com/773292

Я не уверен, если для этого случая достаточно типичного ухода и дезинфекции входных данных, и, будучи дизайнером, у меня нет тонны опыта, связанного с безопасностью. Я знаю, что это должно быть очевидно для меня, но его не!

Как я могу гарантировать, что веб-приложение, которое будет создавать и сериализовать действия, не может передать опасный текст в привилегированный процесс, который получает действия?

Спасибо за помощь
отн

Ответ 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, поэтому вам не нужно беспокоиться о подлых трюках оболочки.

Ответ 2

Если вам нужен не только статус выхода, но и результат, вы, вероятно, захотите использовать Open3.popen3:

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

Дополнительная информация здесь: Получение вывода вызовов system() в Ruby

Ответ 3

Я бы предложил изучить модуль "shellwords". Этот script:

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

Это старый вопрос, но поскольку это в значительной степени единственный реальный ответ, который вы найдете при поиске в Google, я думал, что добавлю оговорку. Многопользовательская версия системы кажется разумно безопасной для Linux, но она НЕ на Windows.

Попробуйте system "dir", "&", "echo", "hi!" в системе Windows. Будут выполняться как dir, так и echo. Разумеется, эхо может быть чем-то гораздо менее безобидным.

Ответ 5

Я знаю, что это старый поток, но есть еще один вариант, который слегка коснулся Саймон Хюрлиманн.

В этой теме не так много информации, и я думаю, что это может помочь другим, нуждающимся в помощи.

В этом примере мы будем использовать Open3, который дает вам возможность запускать команды синхронно или асинхронно и предоставляет stdout, stderr, коды выхода и PID.

Open3 предоставляет вам доступ к stdout, stderr, кодам выхода и потоку для ожидания дочернего процесса при запуске другой программы. Вы можете указать различные атрибуты, перенаправления, текущий каталог и т.д. Программы так же, как и для Process.spawn. (Источник: Open3 Docs)

Я решил форматировать вывод как объект CommandStatus. Это содержит наши stdout, stderr, pid (из рабочего потока) и 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" )) следует этим простые правила:

  • нет метасимволов
  • без зарезервированного слова оболочки и специального встроенного
  • Ruby вызывает команду напрямую без оболочки

Вы можете принудительно вызвать вызов оболочки, добавив ";" к строке (потому что ";" является метасимволом)

Это просто запрещает Ruby бросать ошибку 'spawn': No such file or directory, если вы передадите неверную команду. Вместо этого он передаст это прямо в ядро, где ошибка будет решена изящно и появится как STDERR вместо неперехваченного исключения.