Данная статья не представляет собой всеобъемлющего руководства по программированию безопасных сайтов, она описывает основные принципы безопасности, о которых следует знать при программировании сайтов, и предполагает наличие у читателя базовых навыков веб-программирования и поиска информации в сети. Статья посвящена обзору основных путей взлома веб-сайтов с рекомендациями по защите от них. Упор делается на применение платформы PHP+MySQL, но основные принципы должны быть применимы и для других платформ.
Одним из ключевых принципов безопасности сайта является то, что любым данным, пришедшим извне, доверять нельзя. Даже если данные предварительно проходят проверку с помощью JavaScript в браузере пользователя (и не присылаются в случае его отключения). Даже если это название (User-Agent) браузера пользователя. Даже если это cookie, выставленные нашим же сайтом ранее. Все, что приходит извне, можно подделать. К примеру, один из распространенных форумных движков некогда взламывался из-за того, что злоумышленник посылал запрос с подделанным cookie, вызывая этим SQL-инъекцию (см. далее).
Крайне рекомендуется по прочтении статьи поискать в интернете дополнительную, возможно, новую, информацию по упоминающимся здесь конкретным направлениям взлома.
Загрузка файлов
Сайт может позволять посетителям загружать свои файлы с последующим отображением на своих страницах. Это могут быть, к примеру, изображения в формате JPEG. Важно ограничить типы загружаемых файлов, чтобы вместо картинки не был бы загружен исполняемый файл.
При загрузке файла PHP в переменной $_FILES[' userfile ']['type'] возвращает mime-тип файла, для JPEG-изображения это будет image/jpeg. Может показаться, что проверка этого типа достаточна для уверенности в том, что загружено именно изображение. Также встречается идея пытаться читать файл изображения функциями getimagesize или imаgecreatefromjpeg. Однако тип файла здесь определяется на основе содержания, так что правильное JPEG-изображение, сохраненное с расширением.php, будет определено как image/jpeg. А называться будет xxxxx. php. Веб-сервер же, принимающий решение об обработчике (handler) для того или иного файла, смотрит именно на расширение. Злоумышленник берет корректную картинку, приписывает к ней в конец (или в EXIF-данные) php-скрипт, и сервер его исполняет, мы взломаны.
Таким образом, контролировать для обеспечения безопасности следует именно расширение файла, а проверки через определение mime-type и через попытку открыть файл функцией getimagesize имеют смысл только для контроля того, что вместо картинки не будет загружен мусор, безвредный, но картинкой не являющийся.
Также можно хранить файлы в директории, недоступной для непосредственного доступа посетителей веб-сервера, а отдавать их контент через скрипт. Это, однако же, увеличивает нагрузку на сервер и требует реализации базовой функциональности веб-сервера (выдачи даты последнего изменения и реакции на условные запросы типа "If-Modified-Since", выдачи корректных mime-type и поддержки докачки).
Можно также в настройках веб-сервера для папки с пользовательскими файлами попытаться запретить исполнение скриптов директивой RemoveHandler, однако в ней придется указывать всевозможные расширения для файлов всех поддерживаемых конкретным веб-сервером обработчиков, что чревато пропуском какого-нибудь малоизвестного или нового обработчика.
Register Globals
В PHP есть функциональность "Register Globals" — автоматическое заведение переменных при поступлении их в запросе (GET, POST, COOKIE). То есть, к примеру, скрипт, будучи вызван как script.php?a=hello, напечатает "hello" при включенных register_globals. Если программист не следит за начальной инициализацией переменных, может возникнуть уязвимость, иллюстрируемая простым примером:
if ($login == 'admin' && $password == ' пароль админа ') $is_admin = true; ...... if ($is_admin) { какие-то действия, разрешенные только админу } Неопытному глазу может показаться, что без знания пароля админа мы не установим переменную $is_admin в true, она останется неопределенной, и if($is_admin) не выполнится. Но переменная $is_admin может быть установлена и путем вызова нашего скрипта с аргументом?is_admin=1. И из-за того, что мы пропустили переменную из запроса (например, не написали в начале скрипта $is_admin = false), нас и похакали.
Контролировать такие ситуации легче, если включить в PHP отображение всех ошибок, предупреждений (warnings) и замечаний (notices) директивой error_reporting. Мы бы увидели замечание о том, что используется неинициализированная переменная $is_admin. Это одна из причин к тому, чтобы писать скрипты, включив диагностические сообщения PHP на максимум.
SQL-инъекции
Атаки SQL-инъекциями возможны против сайтов, которые не используют правильное разделение SQL-запросов и вставляемых в них данных. Пояснить суть SQL-инъекций лучше всего на примере.
Пусть у нас есть доска объявлений, на ней регистрируются пользователи, мы разрешаем пользователям через интерфейс удалять свои записи. Вот код PHP-скрипта:
mysql_query('DELETE FROM messages WHERE id='.$message_id.' AND user_id='.$user_id); Пояснения: переменная message_id приходит от ссылки "Удалить" ($_REQUEST['message_id']), в ней содержится идентификатор удаляемой записи (целое число); переменнаяuser_id хранится в сессии, мы записываем в нее идентификатор пользователя при его успешной авторизации на сайте.
Теперь предположим, что хакер подделал адрес ссылки для удаления и вместо «?message_id=15» отправил нам «?message_id=15 OR 1=1». После подстановки этого значения в запрос он станет таким:
DELETE FROM messages WHERE id=15 OR 1=1 AND user_id=3 Мы видим, что данные стали выражением, в выражение попало логическое "или" (OR), в результате чего хакер "выключает" проверку user_id и может удалять чужие записи. В качестве разминки могу предложить придумать такой запрос, которым хакер удалит с нашей доски объявлений все записи разом.
Другой пример: проверка логина и пароля пользователя, которые поступают в переменных $login и $password:
mysql_query('SELECT id FROM users WHERE login="'.$login.'" AND password="'.$password.'"'); Если в $login хакер отправляет «admin" OR 1="1», то его пустят на сайт под логином admin даже без знания пароля:
SELECT id FROM users WHERE login="admin" OR 1="1" AND password="" А если напишет «" OR 1=1 OR 1="1», то его вообще пустят под первым попавшимся в базе данных пользователем.
Таким образом, основным правилом противодействия SQL-инъекциям является недопущение ситуации, когда данные интерпретируются как выражения. Нужно либо насильно приводить значения в ожидаемый тип (функции intval, floatval, если ожидается число), либо просто заключать их всех в кавычки и экранировать содержащиеся внутри них спецсимволы.
В языке PHP есть специальная функция, экранирующая текст перед вставкой в MySQL-запрос, она называется mysql_real_escape_string. Используемые иногда вместо нееmysql_escape_string, addslashes и (да, да, встречаются и такие странные люди) htmlspecialchars либо неэффективны (см. мануал на www.php.net), либо предназначены для другого (как последняя).
Изначально в язык PHP были введены так называемые «Волшебные кавычки» (Magic Quotes). Это функциональность, при которой PHP сам добавляет обратную косую черту перед всеми кавычками (и уже имеющимися обратными косыми чертами) в данных, поступающих скрипту извне. С одной стороны, это некоторым образом защищает скрипт, автор которого не заморачивал себе голову насчет безопасности своего творения. С другой стороны, данные "портятся" все подряд, даже те, которые и не предначены для запросов к базе. Не хочется же видеть на сайте приветствие "Здравствуйте, д\'Артаньян"? И приходится либо чистить данные функцией stripslashes, либо вообще отключатьmagic_quotes в настройках сервера.
Да и защита через magic_quotes не стопроцентна: она не предотвратит первую из приведенных выше инъекций, с конструкцией «WHERE id='.$message_id» — кавычек вокруг аргумента и так нет, хакеру не нужно вставлять закрывающие кавычки. Также не учитывается кодировка соединения с базой данных.
В конце концов, сами авторы PHP не советуют использовать «Magic Quotes» и прекращают их поддержку начиная с 6-ой версии PHP.
Таким образом, уповать на то, что скрипт работает в условиях действующих magic_quotes, не следует, но следует обрабатывать данные перед вставкой в SQL-запросы функциями mysql_real_escape_string / pg_escape_string и обязательно заключать значения в SQL-запросе в кавычки. При этом крайне желательно подвергать такой обработке все данные, участвующие в SQL-запросах, насколько бы надежным ни казался их источник.
Некоторые специалисты советуют применять для этого специальные конструкторы SQL-запросов, которые сами обеспечивают требуемое разделение запроса и данных.
XSS
XSS (Cross Site Scripting, "межсайтовый скриптинг", названный X SS, чтобы не было путаницы с CSS, таблицами стилей) представляет собой атаку, при которой злоумышленник публикует на атакуемом сайте скрипт (к примеру, на языке JavaScript), который исполняется у пользователей сайта при открытии ими страниц. Поскольку этот скрипт выполняется в браузере у пользователя, то он имеет доступ к информации в его cookie, а также может производить на сайте действия от имени пользователя (если тот "залогинен"), к примеру, читать, писать и удалять сообщения.
Очевидно, что в основном XSS угрожает сайтам, на которых регистрируются и оставляют информацию его пользователи (форумы, блоги, доски объявлений), но опасность может угрожать и администраторским интерфейсам, если в них есть модули, предназначенные для просмотра данных, поступающих от посетителей сайта. Это могут быть сообщения из форм обратной связи, заказа или отзывов. Это может быть и статистическая информация об адресах, откуда к нам приходили посетители (поле HTTP-запроса "Referer"), и какими браузерами они при этом пользовались (поле "User-Agent").
Основным способом противодействия XSS-атакам является фильтрация пришедших извне и публикуемых на сайте данных. Как правило, достаточно заменять символы "<" и ">" на "<" и ">" соответственно (php-функция htmlspecialchars), при этом введенный посетителем текст теряет HTML-оформление, а содержащиеся в нем скрипты утрачивают вредоносность.
Правда, не все сайты могут пойти на такое радикальное решение, как игнорирование HTML-разметки. Зачастую, все-таки надо предоставить пользователю возможность как-то оформлять свои сообщения: выделять цитаты, менять шрифты, раскрашивать тексты разным цветом, вставлять картинки и таблички, как, к примеру, сделано на LiveJournal. Им приходится разрабатывать и применять алгоритмы частичной очистки HTML.
XSS и BB-коды
Одним из путей являются так называемые BB-коды (Bulletin Board), альтернативные теги, обычно записываемые в квадратных скобках, которые движок сайта потом при отображении заменяет на HTML-аналоги. Набор этих псевдотегов обычно очень ограничен, иногда они даже не могут иметь атрибутов. Либо набор атрибутов также сильно ограничен. Вообще, можно реализовать различные языки разметки, главное, чтобы они четко контролировались разработчиком.
Таким образом, положительным моментом BB-кодов является то, что их гораздо легче контролировать, чем HTML (который, к тому же, в разных браузерах может поддерживать разные недокументированные конструкции). Потом, один BB-код может заменяться на целую группу HTML-тегов, оформляя цитату, вставляя ссылку на картинку с предпросмотром ее данных (размера) и т. д. Недостатками BB-кодов является то, что это отдельный язык, который надо изучать пользователям сайта, который может по-разному быть реализован на разных сайтах. Ну и уже готовые HTML-тексты перед публикацией придется переводить с HTML на BB.
Но и BB-ориентированные системы могут быть подвержены XSS-атакам, если при их проектировании сделано отступление от идеи написания языка разметки с нуля. Приведу пример: пусть мы тегом [b][/b] оформляем жирный текст. И при этом разрешаем пользователям с помощью атрибута color менять его цвет. То есть пусть [b color="red"]...[/b] заменяется на <b style="color:red">...</b>. На первый взгляд все хорошо, однако...
Злоумышленник может в значение атрибута color записать что-то посложнее простого цвета, к примеру, он туда запишет строку «red" onmouseover=" скрипт » или «expression(скрипт)». И, если мы не осуществляем должной фильтрации значения цвета, мы получим скрипт, выполняющийся у посетителей.
Хорошей идеей для такой фильтрации будет не вычищать потенциально опасные вещи (к примеру, слово expression или кавычки), а наоборот, пропускать только такие данные, которые являются корректными. В случае с цветом это последовательность латинских букв и цифр, возможно, с символом «#» в начале (т. е., к примеру, регулярное выражение «^[#]?[a-zA-Z0-9]+$». Есть, конечно, в стандарте HTML и другие форматы записи цветов, желающие могут справиться в интернете). Можно пойти дальше, отсеивать, к примеру, цвета, равные или близкие к цвету фона и т. д. Всем этим мы добъемся того, что наш фильтр будет правильно работать и в случае появления поддержки в браузерах каких-то конструкций, которых, возможно, еще и в проекте нет.
XSS и HTML
Бывает, что сайт не может пойти на полный запрет использования HTML-верстки в пользовательских сообщениях. В основном это публичные блоги и "социальные сети". Огромное количество пользователей самых разных уровней опытности владения компьютером, самые разные нужды при оформлении публикуемых материалов. Можно сделать достаточно полный язык разметки, но нереально заставить пользователей ему научиться.
Единственный оставшийся путь — разрешать HTML, но фильтровать в нем все конструкции, подозрительные с точки зрения XSS-уязвимости.
Такая фильтрация довольно сложна, ее сложность усугубляется тем, что браузеры допускают значительные вольности при написании HTML-разметки, стилей и скриптов, могут прощать отсутствующие или незакрытые кавычки, кодированные различными алгоритмами символы, лишние пробельные символы и пр. В качестве иллюстрации рекомендую ознакомиться с памяткой по различным способам XSS-атак на сайте ha.ckers.org.
Поэтому чистить HTML от вредоносных конструкций следует в несколько этапов, и первый их них — исправление ошибок синтаксиса. Для этого можно воспользоваться, к примеру, валидатором Tidy. Он расставит отсутствующие кавычки, "проэскейпит" содержащиеся в атрибутах спецсимволы вроде угловых скобок, закроет назакрытые теги, перенесет в секцию <head> (которую мы просто отрежем) все, что не имеет права находиться в <body> и т. д.
Далее уже следует вычищать комментарии, запрещенные теги, атрибуты и стили.
Кроме чисто скриптовых конструкций стоит запретить абсолютное позиционирование в стилях, чтобы не было возможности наложить поддельные кнопки и формы поверх настоящих.
Полная же задача определения потенциальной зловредности того или иного текста, как уже упоминалась, довольно громоздка, возможно, я опишу ее отдельной статьей или скриптом. Отдельно упомяну, что XSS-атаку можно провести через вставленный Flash-ролик, для запрета роликам вызывать JavaScript-команды используйте атрибутAllowScriptAccess тега <embed>:
<embed AllowScriptAccess='never'... type="application/x-shockwave-flash"></embed> Проследить за тем, чтобы не использовались иные type, тоже полезно.
Встретил упоминание о XSS через PNG с дописанным в конец текстом в MSIE. Будем надеяться, что оно пропатчено.
XSS и UTF-7
Отдельно стоит упомянуть использование для XSS-атаки скриптов, написанных в кодировке UTF-7. Эта кодировка замечательна тем, что угловые скобки в ней записываются как «+ADw-» и «+AD4-» и, стало быть, пропускаются функцией htmlspecialchars нетронутыми. Если браузер пользователя настроен на автоопределение кодировки, а ни в заголовках, ни на странице в теге <meta> до любого контента не прописано кодировки, браузер, встретив в контенте характерные для UTF-7-кодировки символы, переключится в эту кодировку и выполнит код.
Например, у нас на форуме название темы идет в тег <title>, секция <head> документов начинается таким образом:
<title>MegaForum - название темы </title> <meta http-equiv="Content-Type" content="text/html; charset=windows-1251" /> Кодировка прописана, но, к сожалению, уже после вывода поступившего от посетителя названия в <title>. И если он, к примеру, назовет тему
+ADw-/title+AD4APA-script+AD4- скрипт +ADsAPA-/script+AD4- то в результате браузер прочитает и проинтерпретирует код
<title>MegaForum - </title><script> скрипт </script> Последствия, думаю, очевидны. А вся проблема из-за того, что <meta> с указанием кодировки не стояла перед <title>. И не было указания кодировки в HTTP-залоговкеContent-Type.
CSRF: совершение действия от имени пользователя
Атакой CSRF (Cross-Site Request Forgery) называется отправка запроса через браузер пользователя с одного сайта на другой, так что атакуемый сайт исполняет этот запрос, как будто он поступил от пользователя.
Лучше попробовать понять принцип этой атаки на примере:
Есть сайт A, пусть это форум, на нем зарегистрированные пользователи могут оставлять сообщения. Для отправки сообщения используется обычная HTML-форма с <textarea>и кнопкой "Отправить".
Есть сайт B, в котором сделана копия этой HTML-формы для отправки сообщения, а в атрибуте action у формы прописан адрес сайта А.
Теперь, если некто, залогинившись на форуме А и получив сессионную cookie, зайдет на сайт B, заполнит форму там и нажмет "Отправить", его сообщение пойдет на форум А, словно оно было отправлено не снаружи, а с самого форума. Пользователь-то уже залогинен.
Остается только заманить пользователя с форума на сайт B и отправить эту форму автоматически – с помощью JavaScript-метода submit(), к примеру.
Первый напрашиващийся способ – проверять поле Referer. Ведь в случае прихода пользователя снаружи, в этом поле будет адрес не форума А, а сайта В.
Но этот способ может не быть эффективным, так как это поле не является обязательным и часто передача его браузером блокируется разными программами-антивирусами и антишпионами. Дело тут, правда, такое, что от пользователей можно и требовать обязательного включения передачи Referer, тем более что часто на этом базируется защита от размещения ссылок на ваш контент на чужих сайтах.
Есть, однако, и другой способ. Дело в том, что на сайте В смогли сделать копию формы с форума А только потому, что она, эта форма, у всех пользователей была одинакова. Стоит добавить в нее невидимое (type="hidden") поле с каким-то уникальным для каждого посетителя параметром, и проверять его при получении этой формы, как мы сможем сказать, что эта форма отправлена с нашего форума А, а не со стороннего ресурса.
Еще один способ: спрашивать ввод CAPTCHA при совершении критических действий вроде смены пароля или перевода денег (как сделано в WebMoney).
Ссылки
|