19 смертных грехов, угрожающих безопасности программ
Шрифт:
В чем состоит грех
Безопасность подвергается серьезной угрозе, когда программист не уделяет должное внимание обработке ошибок. Иногда программа может оказаться в некорректном состоянии, но чаще все заканчивается отказом от обслуживания, так как приложение просто «падает». Эта проблема не утрачивает значимости даже в таких современных языках, как С# и Java, только в них аварийное завершение программы происходит из–за необработанного исключения, а не из–за того, что автор забыл проверить код возврата.
Суровая реальность такова: любая ненадежность программы, приводящая к краху или перезапуску, – это отказ от обслуживания, а следовательно, может угрожать безопасности,
Очень часто причиной подобной ошибки становится некритическое копирование кода из какого–нибудь примера. Ведь обычно в примерах для простоты опускают проверку ошибок.
Подверженные греху языки
Уязвим любой язык, в котором функция извещает об ошибке с помощью кода возврата, например ASP, PHP, С и С++, а также языки, полагающиеся на исключения: С#, VB.NET и Java.
Как происходит грехопадение
Есть шесть способов впасть в этот грех:
□ раскрытие излишней информации;
□ игнорирование ошибок;
□ неправильная интерпретация ошибок;
□ бесполезные возвращаемые значения;
□ обработка не тех исключений, что нужно;
□ обработка всех исключений.
Рассмотрим каждый в отдельности.
Раскрытие излишней информации
Эта тема обсуждается в разных главах книги, а особенно в грехе 13. Ситуация типична: возникает ошибка, и вы из соображений «практичности» сообщаете пользователю все подробности случившегося, а заодно советуете, как ошибку исправить. Только вот беда – хакер получает лакомый кусок: сведения, с помощью которых он может скомпрометировать систему.
Игнорирование ошибок
Функция возвращает код ошибки не просто так, а чтобы вызывающая программа могла отреагировать. Согласны, некоторые значения кодов возврата носят чисто информационный характер и зачастую необязательны. Например, значение, возвращаемое функцией printf, проверяется очень редко; если оно положительно, то равно числу выведенных символов, а–1 означает ошибку. Честно говоря, для вызывающей программы ошибка в printf не слишком существенна.
Но чаще возвращаемое значение важно. Например, в Windows есть несколько функций, позволяющих сменить учетную запись, от имени которой работает программа: ImpersonateSelf, ImpersonateLogonUser и SetThreadToken. Если любая из них возвращает ошибку, значит, олицетворение не состоялось, и поток продолжает работать от имени того же пользователя, что и весь процесс.
Или возьмем ввод/вывод. Если вы вызываете функцию fopenQ, а она не может открыть файл (его не существует или к нему нет доступа), и вы не обрабатываете ошибку, то все последующие вызовы fwrite или fread тоже завершатся неудачно. А если вы читаете из файла данные, а потом как–то их используете, то приложение может «грохнуться».
В языках, поддерживающих исключения, именно они являются основным механизмом для передачи информации об ошибках. Java пытается заставить программиста обрабатывать ошибки, проверяя во время компиляции, что исключения обрабатываются (или, по крайней мере, ответственность за обработку исключения делегируется вызывающей программе). Однако есть исключения, которые могут возбуждаться в самых разных местах, поэтому Java не требует, чтобы они обрабатывались. Типичный пример – NullPointerException. Это печально, так как любое исключение – признак логической ошибки; если оно возникло, то довольно трудно восстановить нормальную работу программы, пусть даже вы его перехватываете.
Но и для тех исключений, которые Java заставляет обработать, компилятор не в состоянии проконтролировать, что вы делаете это сколько–нибудь разумным образом. Часто в этом случае просто завершают программу, даже не пытаясь восстановиться,
Неправильная интерпретация ошибок
Некоторые функции, например recv (для чтения из сокета), ведут себя просто странно. recv может вернуть одно из трех значений. В случае успешного завершения возвращается длина сообщения в байтах. Если в буфере сокета ничего нет и удаленный хост выполнил аккуратное размыкание соединения (orderly shutdown), то recv возвращает 0. В противном случае возвращается–1, а в переменную errno записывается код ошибки.
Бесполезные возвращаемые значения
Некоторые функции из стандартной библиотеки С попросту опасны, например strncpy не возвращает никакого уведомления об ошибке, а лишь указатель на целевой буфер независимо от того, как завершилась операция копирования. Если в результате вызова произошло переполнение буфера, то вы получите указатель на переполненный буфер! Если вам нужен был довод в пользу отказа от использования этих ужасных функций С, так вот он!
Обработка не тех исключений, что нужно
В языках, поддерживающих исключения, надо внимательно относиться к тому, какие именно исключения вы обрабатываете. Например, стандарт С++ гласит:
Функция выделения памяти извещает об ошибке, возбуждая исключение bad_alloc. В этом случае никакая инициализация не проведена.
Но так, к сожалению, бывает не всегда. Например, в библиотеке Microsoft Foundation Classes оператор new в случае ошибки может возбуждать исключение CMemoryException, а многие современные компиляторы С++ (в том числе Microsoft Visual С++ и gcc) позволяют использовать спецификацию std::nothrow, чтобы предотвратить возбуждение исключения оператором new. Поэтому если ваша программа готова обрабатывать исключения типа FooException, а код внутри блока try/catch возбуждает Bar Exception, то программа завершится аварийно, ибо перехватывать Bar Exception некому. Разумеется, можно было бы перехватить все исключения, но это тоже плохо, а почему, мы расскажем в следующем разделе.
Обработка всех исключений
Казалось бы, тема этого раздела – обработка всех исключений – прямо противоположна названию греха «Пренебрежение обработкой ошибок», но на самом деле то и другое тесно связано. Обрабатывать все исключения так же плохо, как вообще не обрабатывать ошибки. Тем самым программа «глотает» ошибки, о которых ничего не знает, которые не может обработать или – и это самое страшное–которые маскируют логические дефекты. Если вы притворяетесь, что никакой ошибки не произошло, то скрытые «баги», о которых вы ничего не знаете, рано или поздно проявятся и программа «умрет» от «непостижимой» причины, да так, что отладить ее будет очень непросто.
Греховность C/C++
В примере ниже автор проверяет неинформативное значение, возвращенное функцией, – strncpy просто возвращает указатель на начало целевого буфера. Эта информация бесполезна.
char dest[19];
char *p = strncpy(dest, szSomeLongDataFromAHax0r, 19);
if (p) {
// все сработало отлично, поинтересуемся значением dest или p
}
Переменная р указывает на начало dest вне зависимости от того, что произошло внутри strncpy. А между тем эта функция не завершает буфер нулем, если длина исходных данных больше или равна размеру буфера dest. При взгляде на этот код закрадывается подозрение, что автор просто не понимает, что именно возвращает strncpy, ожидая, что в случае ошибки получит NULL. Ох, грехи наши тяжкие!