В недавней статье о том, как взламывать сайты, подверженные SQL-инжектам, я рассказывал о программе SQLMAP. Во время написания статьи (точнее перевода и тестирования метода) меня неприятно удивило количество сайтов, в той или иной мере подверженным SQL-инъекциям.
Учитывая популярность SQLMAP и подобных ей инструментов, а также большое количество сайтов, подверженных SQL-инъекциям, я решил написать несколько советов начинающим программистам на PHP, как защитить базу данных от взлома.
Для выявления, подвержен ли скрипт уязвимости, я буду использовать SQLMAP, делать это буду не на локалхосте, а на реальном хостинге. Для ускорения процесса тестирования разных вариантов скриптов, я буду их редактировать прямо на хостинге (Как редактировать файлы прямо на хостинге). Поехали.
Для тестирования создан файл bad.php, который доступен по адресу
То, что передаётся параметру t прямиком отправляется в sql-запрос.
В нём набран заведомо уязвимый скрипт:
<?php $mysqli = new mysqli("localhost", "root", "", "db_library"); if (mysqli_connect_errno()) { printf("Не удалось подключиться: %sn", mysqli_connect_error()); exit(); } else { $mysqli->query("SET NAMES UTF8"); $mysqli->query("SET CHARACTER SET UTF8"); $mysqli->query("SET character_set_client = UTF8"); $mysqli->query("SET character_set_connection = UTF8"); $mysqli->query("SET character_set_results = UTF8"); } $name = filter_input(INPUT_GET, 't'); if ($result = $mysqli->query("SELECT * FROM `members` WHERE name = '$name'")) { while ($obj = $result->fetch_object()) { echo "<p><b>Ваше имя: </b> $obj->name</p> <p><b>Ваш статус:</b> $obj->status</p> <p><b>Доступные для Вас книги:</b> $obj->books</p><hr />"; } } else { printf("Ошибка: %sn", $mysqli->error); } $mysqli->close(); ?>
Подставляем кавычку ;
Получаем ожидаемое
Ошибка: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 1
Тестируем:
Естественно, как и следовало ожидать.
1. Экранирование спец символов
Для mysqli функция экранирования называется real_escape_string. Т.е. «более правильней» наш скрипт должен бы выглядеть так:
<?php $mysqli = new mysqli("localhost", "root", "", "db_library"); if (mysqli_connect_errno()) { printf("Не удалось подключиться: %sn", mysqli_connect_error()); exit(); } else { $mysqli->query("SET NAMES UTF8"); $mysqli->query("SET CHARACTER SET UTF8"); $mysqli->query("SET character_set_client = UTF8"); $mysqli->query("SET character_set_connection = UTF8"); $mysqli->query("SET character_set_results = UTF8"); } $name = filter_input(INPUT_GET, 't'); $name = $mysqli->real_escape_string($name); if ($result = $mysqli->query("SELECT * FROM `members` WHERE name = '$name'")) { while ($obj = $result->fetch_object()) { echo "<p><b>Ваше имя: </b> $obj->name</p> <p><b>Ваш статус:</b> $obj->status</p> <p><b>Доступные для Вас книги:</b> $obj->books</p><hr />"; } } else { printf("Ошибка: %sn", $mysqli->error); } $mysqli->close(); ?>
Всего одна новая строка избавила от всех проблем:
Помните, что экранировать (как и фильтровать) нужно абсолютно все данные, приходящие "с той стороны", т.е. от пользователя. Даже куки, даже те, которые, казалось бы, жёстко прописаны: например, в выпадающем списке три опции — и выбранная опция отправляется на сервер. Казалось, как предопределённая величина может помочь в SQL-инъекциях? Всё очень просто, современные браузеры, даже без специальных плагинов, позволяют редактировать HTML-код на лету. Т.е. вы ожидаете одно из трёх значений, а получаете хитро сконструированный SQL-запрос.
Ещё одна распространённая ошибка — начинающие программисты думают, что есть страница "для всех" — это те, которые отвечают за работу сайта. А есть страницы только для веб-мастера: админка, технические скрипты и т.п. И эти программисты ленятся (или не считают нужным) экранировать/фильтровать данные. Это грубая ошибка — нужно экранировать и фильтровать везде!
2. Фильтрация данных и приведение к ожидаемому типу
Вы обратили внимание, что вместо $_GET[‘t’] я использую filter_input(INPUT_GET, 't')? Я это делаю не просто так.
Вместе с filter_input я могу использовать фильтры. Например очищающие фильтры.
Если мы ожидаем получить только число, то использовать примерно так filter_input(INPUT_GET, 't', FILTER_SANITIZE_NUMBER_INT)
Если мы хотим, чтобы были отброшены все тэги и все специальные символы, то мы можем использовать filter_input(INPUT_POST, 't', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW) в результате чего удаляются все тэги, удаляются и кодируются специальные символы.
Давайте возьмём наш самый первый уязвимый скрипт и попробуем в нём фильтровать данные таким образом:
<?php $mysqli = new mysqli("localhost", "root", "", "db_library"); if (mysqli_connect_errno()) { printf("Не удалось подключиться: %sn", mysqli_connect_error()); exit(); } else { $mysqli->query("SET NAMES UTF8"); $mysqli->query("SET CHARACTER SET UTF8"); $mysqli->query("SET character_set_client = UTF8"); $mysqli->query("SET character_set_connection = UTF8"); $mysqli->query("SET character_set_results = UTF8"); } $name = filter_input(INPUT_POST, 't', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); if ($result = $mysqli->query("SELECT * FROM `members` WHERE name = '$name'")) { while ($obj = $result->fetch_object()) { echo "<p><b>Ваше имя: </b> $obj->name</p> <p><b>Ваш статус:</b> $obj->status</p> <p><b>Доступные для Вас книги:</b> $obj->books</p><hr />"; } } else { printf("Ошибка: %sn", $mysqli->error); } $mysqli->close(); ?>
Результат полностью аналогичен предыдущему — попытка SQLMAP с треском провалилась.
Для строки можно сделать так
filter_input(INPUT_GET, 't', FILTER_SANITIZE_ENCODED, FILTER_FLAG_STRIP_HIGH)
(кодирует в формат URL, удаляет специальные символы).
Или так filter_input(INPUT_GET, 't', FILTER_SANITIZE_MAGIC_QUOTES)
(просто экранируются одиночная кавычка ('), двойная кавычка ("), обратный слэш () и NUL (NULL байт)).
3. Ставьте передаваемые в запрос строки в кавычки
Давайте посмотрим на этот код:
<?php $mysqli = new mysqli("localhost", "root", "", "db_library"); if (mysqli_connect_errno()) { printf("Не удалось подключиться: %sn", mysqli_connect_error()); exit(); } else { $mysqli->query("SET NAMES UTF8"); $mysqli->query("SET CHARACTER SET UTF8"); $mysqli->query("SET character_set_client = UTF8"); $mysqli->query("SET character_set_connection = UTF8"); $mysqli->query("SET character_set_results = UTF8"); } $name = filter_input(INPUT_GET, 't', FILTER_SANITIZE_MAGIC_QUOTES); if ($result = $mysqli->query("SELECT * FROM `members` WHERE name = $name")) { while ($obj = $result->fetch_object()) { echo "<p><b>Ваше имя: </b> $obj->name</p> <p><b>Ваш статус:</b> $obj->status</p> <p><b>Доступные для Вас книги:</b> $obj->books</p><hr />"; } } else { printf("Ошибка: %sn", $mysqli->error); } $mysqli->close(); ?>
Т.е. мы экранируем кавычки, но мы забыли взять в одинарные кавычки переменную $name, которая является частью запроса. А вот результат:
Результат плачевный, не смотря на экранирование кавычек, наш скрипт подвержен SQL-инъекции.
4. Не выводите сообщения об ошибках
Ошибки могут появляться в процессе запроса к базе данных, а также уже на следующих этапах — в процессе обработки полученных значений. Например, был возвращён пустой результат, а скрипт без проверки, сразу пытается его обработать, то появится ошибка.
Если ваши скрипты продолжают оставаться уязвимыми, то отсутствие сообщений об ошибках даст возможность использовать только слепую SQL-инъекцию. Слепая инъекция — это тоже плохо, но это немного усложнит нападающему его задачу.
Если SQL-инжекта нет, а сообщения об ошибках выводятся, то это может дать информацию о файлах, о структуре скриптов. Лучше всего, проверяйте и, если данные после очистки отсутствуют или после запроса базы данных ничего не получено, то отображайте главную страницу.
Не нужно показывать оскорбительных надписей вроде «F*ck you, hacker!», это может обидеть некоторых тестеров. И вместо обычной процедуры «попробовал-не нашёл-переключился на следующий», тестер может взяться за ваш сайт серьёзно.
Рассмотрим такой вариант скрипта:
<?php $mysqli = new mysqli("localhost", "root", "", "db_library"); if (mysqli_connect_errno()) { printf("Не удалось подключиться: %sn", mysqli_connect_error()); exit(); } else { $mysqli->query("SET NAMES UTF8"); $mysqli->query("SET CHARACTER SET UTF8"); $mysqli->query("SET character_set_client = UTF8"); $mysqli->query("SET character_set_connection = UTF8"); $mysqli->query("SET character_set_results = UTF8"); } $name = filter_input(INPUT_GET, 't'); if ($result = $mysqli->query("SELECT * FROM `members` WHERE name = $name")) { while ($obj = $result->fetch_object()) { echo "<p><b>Ваше имя: </b> $obj->name</p> <p><b>Ваш статус:</b> $obj->status</p> <p><b>Доступные для Вас книги:</b> $obj->books</p><hr />"; } } else { // printf("Ошибка: %sn", $mysqli->error); } $mysqli->close(); ?>
Т.е. ничего не фильтруется и даже не поставлены кавычки вокруг $name. Результат следующий:
Т.е. в первый момент sqlmap назвала параметр не способным к инъекциям, но очень скоро спохватилась:
GET parameter 't' seems to be 'MySQL >= 5.0 boolean-based blind - Parameter replace' injectable
Т.е. параметр уязвим к слепой инъекции.
5. Проверяйте все переменный на возможность инжекта
Переменные, про которые часто забывают:
- скрытые поля в формах, которые пользователь, по вашему мнению, не видит и не может отредактировать (будьте уверены — и видит и может отредактировать)
- части адреса страницы (даже если у вас mod_rewrite и адреса страниц вроде site.ru/catalog/page/84, даже при правильной настройки .htaccess, нужно понимать, что остаётся возможность обратиться к вашему сайту по адресу site.ru?razd=catalog&type=page&number=84)
- имена пользователей
- данные передаваемые методом POST
- данные из кукиз
- данные из referer и т.д.
6. Используйте mod_security (или другой файервол для веб-приложений).
mod_security не решит за вас всех проблем, если вы используете скрипты, изобилующие ошибками и дырами. Но, тем не менее, он здорово усилит любой сервер. Рекомендую мои же статьи:
- Как установить ModSecurity (mod_security) на Apache (на Windows)
- Как усилить веб-сервер Apache с помощью mod_security и mod_evasive на CentOS
Заключение
В этой статье я изложил свои знания, позволяющие мне избегать SQL-инъекций при программировании на PHP. Я не претендую на истину в последней инстанции, поэтому буду рад услышать о ваших собственных методах и приёмах.
GOOD MAN, THNX!!!