Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Как стать программистом и избежать детских ошибок Безопасность: SQL-инъекции


Продолжаю цикл статей про методы работы с БД, начатый здесь, здесь и здесь.

Как обещал, сегодня говорим про безопасность в работе с БД. Вот код, с которым мы работали на протяжении всего цикла статей:

$user = db_query_array("SELECT * FROM user WHERE login = '". 
  $_GET['login']."' AND password = '".$_GET['password']."'"); 
if (!$user) 
  echo "Пароль или имя пользователя неверны.";

Где опасность?

SQL-инъекции

Опасность том, что данные, полученные снаружи, вклеиваются в SQL-запрос, который пойдёт на исполнение. Программист видит его в таком виде:

SELECT * FROM user
WHERE login = 'переменная' AND password = 'переменная'

Беда в том, что данные из переменной не передаются в СУБД напрямую, а становятся частью запроса. Это значит, что они могут влиять на запрос.

Например, если входящий передал вместо логина такую строку (в конце пробел):

' AND FALSE OR user_id = 1 -- 

То запрос будет следующим:

SELECT * FROM user
WHERE login = '' AND FALSE OR user_id = 1 –- ' AND password = ''

Упростим его для понимания, что происходит:

  • Часть от «-- » является комментарием — удалим её.
  • Конструкция AND FALSE делает первое условие неважным.
  • Конструкция OR делает второе условие важным вне зависимости от того, что слева.

В сухом остатке получили:

SELECT * FROM user
WHERE user_id = 1

Теперь подставьте вместо единицы желаемый номер (включая администратора, если они входят через то же окошко, что и простые смертные).

Как не плодить таких программ? Есть несколько способов, которые используются.

Действуем «в лоб»

Очевидно, предыдущий код создаст проблему и в том случае, если имя пользователя содержит апостроф. Строку можно вставлять в SQL, но все спецсимволы при этом нужно экранировать.

Экранирование означает замену на специальные последовательности. Например: апострофы и кавычки можно предварить обратным слешем, знаки перевода строки мнемоникой «\n» и т. п. При этом сами обратные слеши, встречавшиеся в строке, также нужно удвоить.

Наш запрос с экранированными данными выглядел бы так:

SELECT * FROM user
WHERE login = '\' AND FALSE OR user_id  = 1 –- ' AND password = ''

Подсветка указывает на то, что разбирается он 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();

Во-вторых, обработку плейсхолдеров можно организовать самостоятельно. Пример от Дмитрия Котерова.

Чем лучше свой вариант?

  1. Стандартный синтаксис обработки запроса с подстановками для mysqli слишком громоздкий без видимых на то причин. Его в любом случае нужно свернуть в собственную функцию или метод класса.
  2. Можно использовать архиудобные решения для более сложных плейсхолдеров. Например, код Котерова поддерживает именованные плейсхолдеры, явное указание их индексов, а также разворачивание массивов и хешей, плюс подстановка констант. Я же в своей реализации отказался от ссылок на константы и использования имён, но добавил разворачивание хешей через «AND».
  3. Реализация связывания (bind) на PHP требует указания типа параметра. Это излишне, поскольку все типы могут быть приведены к строке или NULL. Я только что проверил это на MS SQL 2000 и давно пользуюсь этим для MySQL. Для Oracle и Postgres не уверен, но скорее всего должно сработать — если будете на них рассчитывать, проверьте этот тезис.
  4. Можно делать связывание в одном месте, а использовать его в другом. Например, если функция содержит 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*) является самим запросом, а остальные — его параметрами, подставляемыми на место плейсхолдеров.

Я, кстати, готов опубликовать свою библиотеку, если читатели явно выразят желание её увидеть.

Резюме

  • Если Вы будете дорабатывать чужие программы, то Вы столкнётесь с разными из этих методов.
  • Если будете проектировать свой фреймворк (или дорабатывать для себя чужой) — выбирайте вариант с плейсхолдерами.
  • Какой бы Вы метод не использовали, выберите такие правила работы, чтобы ошибки как можно слабее зависели от «запаренности» программиста.

Опрос

Меня интересует вопрос: «В каких Вы отношениях с ООП (в языках, его поддерживающих)?»

Если Вы пользуетесь ЖЖ, то проголосуйте там пожалуйста, иначе — можно написать мне.


Этот выпуск Вы можете прокомментировать в Живом Журнале.

Символика и традиция — небольшая заметка о подчёркивании ссылок в Веб-дизайне.

Задать вопрос

Вы можете задать мне любой вопрос. Все полученные мною вопросы могут быть опубликованы в рассылке. Если Вы желаете скрыть свои личные данные из вопроса — укажите это в тексте письма, поскольку в дальнейшем, письма будут публиковаться с полными подписями (без емейлов, разумеется).


В избранное