Безопасное программирование CGI. Последнее обновление: 27/10/1997 ------------------------------------------------------------------------------- Недавние обнаружения дыр в безопасности в широко используемых CGI- сценариях означает, что существующие документы по безопасности CGI не получили должного распространения. Эти сценарии были использованы людьми, не имеющими должного опыта в программировании и не понимающими, что они таким образом открывают свои сервера для атак. Этот документ предназначен для начинающих или средних программистов CGI. Он не претендует на всеобъемлющий анализ безопасности; его цель - помочь людям избежать наиболее распространенных ошибок. Англоязычный оригинал этого документа и другие ресурсы по безопасности CGI находятся по адресу http://www.go2net.com/people/paulp/cgi-security/. Присылайте любые комментарии Паулю Филлипсу . Вопрос: Почему я должен волноваться? Сервер работает с привилегиями nobody, не так ли? Это означает, что вы не можете совершить ничего опасного, даже если взломаете сценарий CGI. Ответ: Неверно. Вот некоторые из действий, которые могут быть предприняты в различных обстоятельствах: 1) Передача по электронной почте файла пароля злоумышленнику (если только он не затенен); 2) Передача злоумышленнику по электронной почте структуры файловой системы; 3) Передача злоумышленнику по электронной почте информации из /etc и т.п.; 4) Запуск login-сервера на привилегированном порту и вход на него по протоколу telnet; 5) Множество атак, связанных с отказом от обслуживания: например, массированный поиск по файловой системе или иные команды, приводящие к загрузке ресурсов; 6) Стирание и/или изменение файлов регистрации (log) сервера. Другая проблема заключается в том, что в некоторых узлах Web-серверы работают с привилегиями суперпользователя (root). У МЕНЯ НЕ ХВАТАЕТ СЛОВ, ЧТОБЫ ОБЪЯСНИТЬ, НАСКОЛЬКО ЭТО ПЛОХО. Вы сами рубите сук, на котором сидите. Какова бы ни была проблема, которая заставила Вас поступить таким образом, Вы должны тем или иным способом ее разрешить, в противном случае она приведет к неприятностям в будущем. Существовало некоторое недопонимание того, что означает "работа Вашего Web-сервера с привилегиями суперпользователя". Очень хорошо запустить Web- сервер от имени root - это необходимо для привязки (bind) к 80 порту 80 UNIX-систем. Однако потом с помощью вызова setuid (установка идентификатора) Web-сервер должен отказаться от этой привилегии. Файл конфигурации Web-сервера должен предоставить Вам возможность задать пользователя, от имени которого он может быть запущен; по умолчанию это обычно nobody (никто) - обобщенный непривилегированный пользователь. Следует понимать, что при этом не имеет значения, кто из пользователей фактически является владельцем исполняемого файла, и в программе не должен быть установлен бит setuid. Однако существует хороший аргумент в пользу того, что серверы в действительности не должны работать с безымянным пользователем nobody, а должны иметь присвоенный Web-серверу конкретный UID (идентификатор пользователя) и GID (идентификатор группы) типа "www". Это предотвращает доступ к файлам Web- сервера других программ, которые работают от имени nobody. Существуют программа, называемая "cgiwrap" , которая исполняет сценарии CGI с привилегиями лица, которое ими владеет. Хотя программа cgiwrap успешно преодолевают целый ряд проблем, связанных со сценариями CGI, она в то же время усугубляет влияние недостатков системы защиты. Если злоумышленник сможет ввести команду с использованием от имени некого UID, то команда rm -rf ~ , состоящая всего из нескольких символов, приведет к очевидному результату. Вопрос: Теперь я действительно напуган, может быть мой код тоже имеет ошибки. Не могли бы Вы привести мне несколько примеров дыр в системе защиты? Ответ: Вот это уже дело. Вся философия безопасности CGI может быть сконцентрирована в нескольких словах: "Никогда не верь, что входные данные правильны". Большинство дыр в системе защиты используется путем передачи в сценарий данных, которые его автор не ожидал. Рассмотрим некоторые примеры. Foo хочет, чтобы корреспонденты передавали ей сообщения электронной почты через web. У нее имеется несколько различных адресов электронной почты, поэтому она написала один элемент формы, и будущем она сможет легко его изменять не меняя самого CGI-сценария: Теперь она пишет сценарий под названием "email-foo" и просит администратора системы его установить. (Ей нужно получать разрешение своего системного администратора для установки или изменения сценариев CGI - какая незадача!). Через несколько недель системный администратор звонит Foo и сообщает, что злоумышленники проникли в машину через сценарий Foo! В чем была ошибка Foo? Давайте рассмотрим ошибку Foo на трех различных языках. Foo поместила данные, подлежащие передаче по электронной почте, во временный файл, а ее адрес вошел в состав переменной. Perl: system("/usr/lib/sendmail -t $foo_address < $input_file"); C: sprintf(buffer, "/usr/lib/sendmail -t %s < %s", foo_address, input_file); system(buffer); C++: system("/usr/lib/sendmail -t " + FooAddress + " < " + InputFile); Во всех трех случаях система вызывает командный интерпретатор (оболочку). Foo наивно предполагала, что корреспонденты будут вызывать этот сценарий только из "ее" формы и поэтому адрес электронной почты всегда будет одним из ее адресов. Однако злоумышленник скопировал форму в свою машину, а затем отредактировал ее, так что она стала выглядеть следующим образом: После чего он передал (submit) эти данные на машину Foo, и дальнейшее имеет уже чисто исторический интерес, как и сама машина. Вопрос: Я никогда не использовал вызов system(). Поэтому я полагаю, что мои сценарии находятся в полной безопасности! Ответ: System() не является единственной функцией, которая вызывает командный интерпретатор. В Perl Вы можете вызвать его через конвейер, с помощью регулярных символов подстановки команд ` (backticks) или вызова exec() (в ряде случаев): 1) open (OUT, "|program $args"); 2) `program $args`; 3) exec ("program $args"); В Perl Вы можете нарваться на неприятности даже с оператором eval или с модификатором регулярного выражения /e (который вызывает eval). Этот вариант выходит за рамки данного документа, но будьте все же осторожны. В C/C++ вызов popen(3) также запускает оболочку: popen("program", "w"); Вопрос: Как же нужно поступать правильно? Ответ: в общем виде на этот вопрос можно дать два ответа : используйте данные только тогда, когда это не может Вам повредить, либо проверяйте их, чтобы убедиться, что они безопасны. 1) Избегайте вызова оболочки: open(MAIL, "|/usr/lib/sendmail -t"); print MAIL "To: $recipient\n"; Теперь недостоверные данные более не попадают в оболочку. Однако они будут направлены непроверенными в sendmail. В некотором смысле вы обмениваете проблемы, связанные с оболочкой, на проблемы внешней программы, поэтому позаботьтесь о том, чтобы ее нельзя было обмануть с помощью непроверенных данных! Например, если Вы используете /usr/ucb/mail вместо /usr/lib/sendmail, то символы ~ в некоторых версиях используются для исполнения команд. Будьте осторожны. Вы можете использовать функции Perl system() и exec() без активизации оболочки, для чего необходимо использовать более одного аргумента: system('/usr/games/fortune', '-o'); Для достижения цели, аналогичной в popen(), Вы можете использовать open(), не вызывая при этом командный интерпретатор, для чего следует выполнить open(FH, '|-') || exec("program", $arg1, $arg2); 2) Избегайте непроверенных данных. unless($recipient =~ /^[\w@\.\-]+$/) { # Индикация ошибки Exit (1); } Теперь мы убеждены в том, что данные безопасны, и их можно передать в оболочку. Приведенный пример regexp указывает какие символы являются безопасными, а какие - нет. if ($to =~ tr/;<>*|`&$!#()[]{}:'"//) { # Индикация ошибки Exit (1); } Чтобы вообще исключить метасимволы, а не только их обнаружить, можно использовать приведенную ниже подпрограмму: sub esc_chars { # изменит, например, a!!a to a\!\!a @_ =~ s/([;<>\*\|`&\$!#\(\)\[\]\{\}:'"])/\\$1/g; return @_; } [ДОПОЛНЕНИЕ! Чтобы подчеркнуть, что более надежно задавать явно как раз неопасные символы, а не опасные, мне было указано на несколько промахов в приведенном выше примере regexp. Во первых, символ ^ в некоторых оболочках работает как конвейер, и его также следует избегать. Во вторых, не указан символ \n (новая строка), который, в зависимости от обстоятельств, может разделять команды оболочки. И, возможно, самое опасное - символ \ может присутствовать во входных данных следующим образом: foo\;bar что приведет к замене на foo\\;bar и, таким образом, опасный символ ; опять попадет в оболочку как метасимвол. Заметьте, что я НЕ изменил программу esc_chars в свете данной информации, поэтому не используйте ее в том виде, как она приведена. ДОПОЛНЕНИЕ от 13 июля 1997 г.: прополка продолжается. Regexp исключает также метасимвол ? (который почти так же опасен, как и *) и символ с кодом 255, который воспринимается некоторыми оболочками как разделитель.] Эти примеры указывают, что же является небезопасным. Я полагаю, что они являются полным перечнем потенциально опасных метасимволов, но у меня нет никакого авторитетного источника для проверки. Разница между двумя последними regexp и первым из них представляет собой разницу между двумя политиками безопасности: "запрещено все, что явно не разрешено" и "разрешено все, что явно не запрещено". Любой специалист по обеспечению безопасности подтвердит Вам, что первый принцип приводит к большему уровню безопасности. Для обеспечения максимального уровня безопасности используйте везде, где это возможно, советы 1 и 2. ИСПОЛЬЗУЙТЕ ПРОВЕРКИ TAINT В PERL Perl может оказаться весьма полезным при решении этих проблем. Для активизации проверок безопасности кода запустите perl -T (taint - загрязнение); сведения относительно проверок taint приведены на странице справки (man) по Perl. (Опция T существует только для Perl5.) НЕ ДЕЛАЙТЕ ПРЕДПОЛОЖЕНИЙ ОТНОСИТЕЛЬНО ПЕРЕМЕННЫХ ОКРУЖЕНИЯ Поскольку программы cgi-bin традиционно исполняются в стерильных условиях, обеспечиваемых WEB-сервером, то в многопользовательских системах появляется возможность исполнения Ваших программ cgi-bin другими пользователями, либо возможность форсирования их исполнения в неожиданном контексте. Для предотвращения такой ситуации программы cgi-bin (особенно с битом suid) должны соответствующим образом стерилизовать переменный окружения до того, как будут генерироваться какие-либо вызовы оболочки или любых других программ. Как минимум следует установить значения переменных PATH и IFS в известное состояние: $ENV{"PATH"} = "/bin:/usr/bin:/usr/local/bin"; $ENV{"IFS"} = "/"; Больших усилий потребует установка окружения в пустое состояние с помощью undef() с последующим формированием полностью известного окружения, однако это может оказаться наиболее безопасным способом достижения цели. Заметьте, что perl в режиме проверки taint предостережет Вас, если Вы попытаетесь вызвать что-либо через system() без предварительного соответствующего задания PATH и IFS. Вопрос: Могу ли я доверять данным пользователя, если оболочка не используется? Ответ: Нет. Здесь имеются и другие проблемы. Рассмотрим данный фрагмент кода perl: open(MANPAGE, "/usr/man/man1/$filename.1"); Здесь предпринята попытка разрешить HTML-доступ к страницам man. Что, однако, произойдет, если имя файла пользователя имеет вид ../../../etc/passwd ? Каждый раз, когда Вы имеете дело с именами маршрутов, не забывайте проверить ... их составные части. Вопрос: Что может быть еще? Ответ: В C и C++ при неправильном выделении памяти может произойти переполнение буфера. Perl динамически расширяет свои структуры данных с целью предотвращения такого переполнения. Представьте себе код следующего вида: int foo() { char buffer[10]; strcpy(buffer, get_form_var("feh")); ... } При записи этого кода автор явно предполагал, что значение переменной feh будет занимать менее 10 символов. К несчастью для него, он в этом не убедился, и длина оказалась значительно больше. Это означает, что данные пользователя попадут в программный стек, что в некоторых случаях может использоваться для активизации команд. Это очень трудно обнаружить и Вы, возможно, никогда не столкнетесь с этим. Тем не менее стоит упомянуть о том, что весьма аналогичный прокол был обнаружен ранее, в 1995 году, в NCSA httpd 1.3. Отсутствие проверки такого рода обстоятельств является признаком плохого стиля программирования. В этом же смысле ни при каких обстоятельствах никогда не следует использовать функцию C gets(). Это в принципе небезопасно, поскольку невозможно заранее определить величину входного буфера. Вместо этого используйте fgets() в для stdin. Вопрос: Мой сервер WWW не работает на платформе unix. Только unix обладает этими неприятными дырами в безопасности. Ответ: Это может быть так и не так. Автор данного документа обладает ограниченным опытом работы с серверами на других платформах, однако он более чем сомневается, что там отсутствуют проблемы безопасности. По крайней мере проблемы gets() и переполнения стека присутствуют как в Windows, так и в MacOS. Мы будем рады получить другие примеры опасностей CGI для других платформ. Приведенные ниже конкретные примеры предоставлены Дейвом Андерсеном. В других платформах были обнаружены не только переполнения буферов, но и зияющие проколы в безопасности ряда сценариев cgi-bin (особенно в Windows). Языки сценариев типа perl широко используются в WEB-серверах на основе Windows. При этом отсутствие порта telnet не является определяющим для злоумышленника, который имеет в своем распоряжении все средства для исполнения любых программ на атакованном WEB-сервере. Вполне вероятно, что WEB-серверы Windows NT станут основными целями в будущем, поскольку они не были достаточно тщательно протестированы. Они представляют собой также относительно легкодоступные мишени для атак типа отказа в обслуживании, поэтому особое внимание программистов должно быть направлено на исполняемые на данных машинах сценарии cgi во избежание неправильного использования ресурсов, поскольку последнее может дать в руки злоумышленнику способ для прекращения работы машины. (Относитесь ответственно к свободной памяти, всегда проверяйте объем входных данных, обеспечьте, чтобы в случае ненормального прекращения работы, обусловленного входными данными пользователя, использованные ресурсы всегда освобождались). Приложение Дополнения к данному документу принимаются по адресу . Благодарности тем, кто внес вклад в этот документ: John Halperin Maurice L. Marvin Dave Andersen Zygo Blaxell Joe Sparrow Keith Golden James W. Abendschan Jennifer Myers Jarle Fredrik Greipsland David Sacerdote (c) Перевод: М. Савельев, П. Семьянов. Замечания по переводу присылайте по адресу .