Как стать программистом и избежать детских ошибок Безопасность: SQL-инъекции
Продолжаю цикл статей про методы работы с БД, начатый здесь, здесь и здесь.
Как обещал, сегодня говорим про безопасность в работе с БД. Вот код, с которым мы работали на протяжении всего цикла статей:
$user = db_query_array("SELECT * FROM user WHERE login = '".
$_GET['login']."' AND password = '".$_GET['password']."'");
if (!$user)
echo "Пароль или имя пользователя неверны.";
Где опасность?
SQL-инъекции
Опасность том, что данные, полученные снаружи, вклеиваются в SQL-запрос, который пойдёт на исполнение. Программист видит его в таком виде:
SELECT*FROM user
WHERElogin='переменная'ANDpassword='переменная'
Беда в том, что данные из переменной не передаются в СУБД напрямую, а становятся частью запроса. Это значит, что они могут влиять на запрос.
Например, если входящий передал вместо логина такую строку (в конце пробел):
'AND FALSE ORuser_id=1--
То запрос будет следующим:
SELECT*FROM user
WHERElogin=''ANDFALSEORuser_id=1–- ' AND password = ''
Упростим его для понимания, что происходит:
Часть от «-- » является комментарием удалим её.
Конструкция AND FALSE делает первое условие неважным.
Конструкция OR делает второе условие важным вне зависимости от того, что слева.
В сухом остатке получили:
SELECT*FROM user
WHEREuser_id=1
Теперь подставьте вместо единицы желаемый номер (включая администратора, если они входят через то же окошко, что и простые смертные).
Как не плодить таких программ? Есть несколько способов, которые используются.
Действуем «в лоб»
Очевидно, предыдущий код создаст проблему и в том случае, если имя пользователя содержит апостроф. Строку можно вставлять в SQL, но все спецсимволы при этом нужно экранировать.
Экранирование означает замену на специальные последовательности. Например: апострофы и кавычки можно предварить обратным слешем, знаки перевода строки мнемоникой «\n» и т. п. При этом сами обратные слеши, встречавшиеся в строке, также нужно удвоить.
Наш запрос с экранированными данными выглядел бы так:
SELECT*FROMuserWHERElogin='\' AND FALSE OR user_id = 1 –- 'ANDpassword=''
Подсветка указывает на то, что разбирается он SQL-сервером так, как и задумано.
В PHP экранирование делает функция addslashes. Большинство разработчиков решают проблему «в лоб»:
$_GET['login'] = addslashes($_GET['login']);
$_GET['password'] = addslashes($_GET['password']);
$user = db_query_array("SELECT * FROM user WHERE login = '".
$_GET['login']."' AND password = '".$_GET['password']."'");
if (!$user)
echo "Пароль или имя пользователя неверны.";
Что плохо?
Увеличился объём кода. Разумеется, увеличился не за счёт решения большего количества прикладных задач, за счёт разрешения проблем самого программирования. В позапредыдущей статье я объяснил, почему это плохо.
Есть шанс забыть экранировать данные для некоторых запросов. У малоопытных программистов этот сценарий как раз и случается.
Чрезвычайно часто случается двойное кодирование (по недосмотру программиста, разумеется), когда лишние слеши попадают в саму базу данных и затем портят вид при выводе.
Вторая проблема весьма серьёзная, поэтому должна быть решена так, чтобы не требовалось превращения программиста в сверхчеловека. Система должна компенсировать человеческие слабости.
Есть решение, которое можно встретить в CMS с неоднозначным для русского уха названием Moodle.
Огораживаемся
Все переменные, с которыми работает система должны быть экранированы для СУБД и точка. Если требуется операция по разбору строки (а практика показывает, что требуется она очень и очень редко), то на время строка декодируется (stripslashes).
Соответственно, все данные поступают в программу уже экранированными. В PHP для этого есть параметры magic_quotes_gpc и magic_quotes_runtime. В случае, если они отключены, первый можно сымитировать, второй включить на ходу.
Чем плохо?
Проблема забывания переезжает на этап вывода. В этом случае безопасность не страдает, но страдает внешний вид и восприятие качества программы.
Существует теоретическая вероятность, что временно раскодированная переменная попадёт в запрос, и хакер узнает об этой особенности.
Ставим автозащиту непосредственно у базы
А теперь вспомним код на Perl из прошлого выпуска. Запрос там вызывается в два этапа.
Если забыть про короткие вызовы, вроде «db_query_array», код на Perl будет выглядеть так:
my$out=$dbh->prepare("SELECT * FROM user WHERE login = ? AND password = ?");$out->execute($login,$password);
При этом параметры будут закодированы так, как должны без наших указаний и подставлены на место вопросительных знаков, которые называются плейсхолдерами (placeholder). Таким образом всё, что нам нужно, это взять за правило никогда никаких переменных не вклеивать в запросы напрямую: лучше лишний раз закодировать безопасные данные, чем оставить дыру в программе. Придерживаться этого правила несравнимо проще, чем вручную кодировать
Как сделать в PHP?
Во-первых, разработчики PHP рекомендуют использовать расширение mysqli вместо mysql [пруфлинк: If you are using MySQL versions 4.1.3 or later it is strongly recommended that you use the mysqli extension instead.]
Это расширение поддерживает плейсхолдеры:
// Загружаем запрос
$mysqli->prepare(
"SELECT * FROM user WHERE login = ? AND password = ?")
or /* обработка ошибки */;
// Связываем плейсхолдеры со значениями
// Первый параметр функции указывает типы передаваемых данных.
// В данном случае – две строки.
$stmt->bind_param("ss", $_GET['login'], $_POST['password']);
// Выполняем запрос
$stmt->execute();
Во-вторых, обработку плейсхолдеров можно организовать самостоятельно. Пример от Дмитрия Котерова.
Чем лучше свой вариант?
Стандартный синтаксис обработки запроса с подстановками для mysqli слишком громоздкий без видимых на то причин. Его в любом случае нужно свернуть в собственную функцию или метод класса.
Можно использовать архиудобные решения для более сложных плейсхолдеров. Например, код Котерова поддерживает именованные плейсхолдеры, явное указание их индексов, а также разворачивание массивов и хешей, плюс подстановка констант. Я же в своей реализации отказался от ссылок на константы и использования имён, но добавил разворачивание хешей через «AND».
Реализация связывания (bind) на PHP требует указания типа параметра. Это излишне, поскольку все типы могут быть приведены к строке или NULL. Я только что проверил это на MS SQL 2000 и давно пользуюсь этим для MySQL. Для Oracle и Postgres не уверен, но скорее всего должно сработать если будете на них рассчитывать, проверьте этот тезис.
Можно делать связывание в одном месте, а использовать его в другом. Например, если функция содержит SQL запрос, WHERE условие к которому может быть произвольным (то есть передаётся в функцию уже готовым), то мы можем использовать функцию, ответственную за обработку плейсхолдеров, для того, чтобы безопасно сформировать это WHERE вне функции.
Таким образом, исходный код с использованием моей библиотеки будет выглядеть так:
$user = db_query_array(
"SELECT * FROM user WHERE login = ? AND password ?",
$_GET['login'], $_GET['password']);
if (!$user)
echo "Пароль или имя пользователя неверны.";
Можете заметить, что первый параметр функции db_query_array (и всех остальных db_query*) является самим запросом, а остальные его параметрами, подставляемыми на место плейсхолдеров.
Я, кстати, готов опубликовать свою библиотеку, если читатели явно выразят желание её увидеть.
Резюме
Если Вы будете дорабатывать чужие программы, то Вы столкнётесь с разными из этих методов.
Если будете проектировать свой фреймворк (или дорабатывать для себя чужой) выбирайте вариант с плейсхолдерами.
Какой бы Вы метод не использовали, выберите такие правила работы, чтобы ошибки как можно слабее зависели от «запаренности» программиста.
Опрос
Меня интересует вопрос: «В каких Вы отношениях с ООП (в языках, его поддерживающих)?»
Если Вы пользуетесь ЖЖ, то проголосуйте там пожалуйста, иначе — можно написать мне.
Вы можете задать мне любой вопрос. Все полученные мною вопросы могут быть опубликованы в рассылке. Если Вы желаете скрыть свои личные данные из вопроса укажите это в тексте письма, поскольку в дальнейшем, письма будут публиковаться с полными подписями (без емейлов, разумеется).