Защита сайта от взлома: предотвращение SQL-инъекций

В недавней статье о том, как взламывать сайты, подверженные 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

Тестируем:

01

Естественно, как и следовало ожидать.

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();
?>

Всего одна новая строка избавила от всех проблем:

61

Помните, что экранировать (как и фильтровать) нужно абсолютно все данные, приходящие "с той стороны", т.е. от пользователя. Даже куки, даже те, которые, казалось бы, жёстко прописаны: например, в выпадающем списке три опции — и выбранная опция отправляется на сервер. Казалось, как предопределённая величина может помочь в 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();
        ?>

62

Результат полностью аналогичен предыдущему — попытка 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, которая является частью запроса. А вот результат:

63

Результат плачевный, не смотря на экранирование кавычек, наш скрипт подвержен 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. Результат следующий:

64

Т.е. в первый момент 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 не решит за вас всех проблем, если вы используете скрипты, изобилующие ошибками и дырами. Но, тем не менее, он здорово усилит любой сервер. Рекомендую мои же статьи:

Заключение

В этой статье я изложил свои знания, позволяющие мне избегать SQL-инъекций при программировании на PHP. Я не претендую на истину в последней инстанции, поэтому буду рад услышать о ваших собственных методах и приёмах.

Оставьте комментарий