Практические советы по предотвращению уязвимостей вида sql-injection
Автор: | Иван Коржавин |
email: | korjavin@yandex.ru |
Общая идея.
В процессе работы нашего веб приложения мы работаем с данными, которые получаем от наших пользователей.
Это могут быть html формы, http get/post запросы, ajax запросы, и прочее.
При недостаточной проверке внешних данных, злоумышленник может подобрать такой набор, который заставит наш код выполнить не планируемое действие.
Атак такого типа много, это и переполнение стека, и перегрузка системы ведущая к отказам в обслуживании, и другие.
Мы рассмотрим узкий класс таких атак sql injection или "внедрение sql" как самый распространенный.
Простейший пример
В самом простейшем виде, атака выглядит так:
- У нас есть html форма, которая принимает данные от пользователя, например его e-mail
- Код который обрабатывает эту форму, делает запрос к нашей базе данных, добавляя запись с e-mail пользователя в таблицу.
- Злоумышленник передает нам в html форме строку
1'); drop table users; /*
Тогда наш код отправит в базу данных такой запросinsert into sometable (email) values ('1'); drop users; /* ')
Который помимо вставки записи еще и удалит нам таблицу users.
Основные моменты: злоумышленник использовал точку с запятой для разделения запросов, и знак комментария для отбрасывания хвоста запроса.
Пример посложнее
Пусть у нас есть отдельная часть сайта для администраторов. При попытке войти в нее мы запрашиваем особый логин и пароль.
Используемый запрос:
Вместо перебора логин пароль, злоумышленник передаст вместо login строчку
Admin' --
Получив запрос:
select * from admins where login='Admin' -- ' and pass=''
Так как, двойной дефис это начало однострочного комментария, неважно какой пароль у пользователя Admin, запрос будет успешным.
Пример посложнее с кодировкой
Форма аналогичная предыдущей, но каким либо образом в форму запрещено вводить пробелы и кавычки.
Злоумышленник передает строку:
Admin%27/*test*/--+
В этой строке, %27 способ передать кавычку, и два способа передать пробел. Первый с помощью комментария, а для СУБД mysql любой комментарий разделитель. Второй с помощью символа +
Рубежи обороны
Разберем основные рубежи, где мы можем фильтровать эти атаки:
- Проверка данных на стороне браузера клиента с помощью javascript.
- Проверка введенных пользователем данных на стороне сервера - web сервера.
- Проверки введенных пользователем данных на стороне скрипта - интерпретатора php.
- Изменение способа формирования строки запроса
Первый пункт - валидация данных с помощью браузерных скриптов, может быть отброшен сразу, клиент всегда может отключить любой javascript в своем браузере, или совсем не использовать браузер для посылки запроса, а использовать библиотеки типа curl.
Второй пункт - web сервер, скорее всего, первым принимает данные пользователя, и уже здесь мы можем произвести некоторые, простейшие фильтрации: обрезать query string по стоп-словам.
- Пример для apache:
- Пример для nginx:
По моему мнению, это очень слабый способ. Мы никогда не сможем предусмотреть все варианты передаваемых данных, и более того многие из них, будут абсолютно легальны и не связаны с атаками в определенных контекстах.
К тому же данные могут передаваться закодировано-искаженным способом и различными методами post/put.
Этот способ имеет смысл только в очень ограниченных сценариях. Например, в результате анализа лог-файлов своего вебсервера вы увидели достаточно длинную повторяющуюся строку, что то вроде
%27+having+1%3D1--+
то можно ее заблокировать, только для того что бы не нагружать бесполезной работой остальные части вашего приложения. Но надеяться на этот способ не следует.
Третий пункт - скрипт php. В коде php мы смело вставляем полученные от пользователя данные в запрос, что категорически нельзя делать. Мы обязаны проверить переданные нам параметры с помощью регулярных выражений, или пропустить их через функции удаляющие экранирование, и прочее, например mysql_real_escape_string.
Этот способ уже дает кое-какую гарантию, и я всегда рекомендую проверять данные от пользователей. Продумайте заранее, какие именно данные допустимы, и проверяйте их в самом начале, до передачи в какие либо функции.
Четвертый способ - изменение способа формирования запроса. Например:
В этом примере мы используем некий шаблон для sql, который заполняем данными с помощью функции sprintf, а она проведет для нас проверку типов.
Но есть способ еще лучше. Использовать параметры sql. Пример:
В этом примере много плюсов по сравнению с предыдущими пунктами. Во первых, мы по прежнему проверяем тип параметра, функцией bind_param, во вторых мы строим запрос с помощью СУБД без пользовательских данных.
Это означает что запрос который мы передали с помощью функции prepare, будет разобран на стороне mysql заранее. Будет составлен его план, и запрос преобразован в структуры mysql. Поэтому, если пользовательские данные каким то образом попробуют "сломать" логику запроса, он не будет выполнен, и поэтому же пользовательские данные не будут рассматриваться как управляющие команды sql.
Дополнительным плюсом этого метода будет то, что если запрос нужно выполнить несколько раз с разными данными, вам не нужно будет конструировать и передавать его в СУБД, вы просто передадите следующий набор параметров, и сэкономите время на процедурах генерации запроса в вашем скрипте, и на разборе запроса со стороны СУБД.
Очень рекомендую использовать этот способ - sql запросы с параметрами.