SQL-инъекции

Многие веб-разработчики даже не догадываются, что SQL-запросы могут быть подделаны, и считают, что SQL-запросы всегда достоверны. На самом деле поддельные запросы могут обойти ограничения доступа, стандартную проверку авторизации, а некоторые виды запросов могут дать возможность выполнять команды операционной системы.

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

Благодаря отсутствию проверки пользовательского ввода и соединению с базой данных под учетной записью суперпользователя (или любого другого пользователя, наделенного соответствующими привилегиями), взломщик может создать еще одного пользователя БД с правами суперпользователя.

Пример #1 Постраничный вывод результата... и создание суперпользователя в PostgreSQL

<?php

$offset 
$argv[0]; // внимание, нет проверки вводимых данных!
$query  "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";
$result pg_query($conn$query);

?>
Обычно пользователи кликают по ссылкам 'вперед' и 'назад', вследствие чего значение переменной $offset заносится в URL. Скрипт ожидает, что $offset - десятичное число. Однако, взломщик может попытаться взломать систему, присоединив к URL дополнительную строку, обработанную функцией urlencode():
0;
insert into pg_shadow(usename,usesysid,usesuper,usecatupd,passwd)
    select 'crack', usesysid, 't','t','crack'
    from pg_shadow where usename='postgres';
--
Если это произойдет, скрипт предоставит взломщику доступ к базе с правами суперпользователя. Заметим, что значение 0; использовано для того, чтобы задать правильное смещение для первого запроса и корректно его завершить.

Замечание:

Часто используемой техникой для игнорирования SQL-парсером оставшейся части запроса является использование --, означающей комментарий.

Еще один вероятный способ получить пароли учетных записей в БД - атака страниц, предоставляющих поиск по базе. Взломщику нужно лишь проверить, используется ли в запросе передаваемая на сервер и необрабатываемая надлежащим образом переменная. Это может быть один из устанавливаемых на предыдущей странице фильтров, таких как WHERE, ORDER BY, LIMIT и OFFSET, используемых при построении запросов SELECT. В случае, если используемая вами база данных поддерживает конструкцию UNION, взломщик может присоединить к оригинальному запросу еще один дополнительный, для извлечения пользовательских паролей. Настоятельно рекомендуем использовать только зашифрованные пароли.

Пример #2 Листинг статей... и некоторых паролей (для любой базы данных)

<?php

$query  
"SELECT id, name, inserted, size FROM products
           WHERE size = '
$size'";
$result odbc_exec($conn$query);

?>
Статическая часть запроса может комбинироваться с другим SELECT-запросом, который выведет все пароли:
'
union select '1', concat(uname||'-'||passwd) as name, '1971-01-01', '0' from usertable;
--
Если этот запрос (использующий ' и --) присоединить к значению одной из переменных, используемых для формирования $query, то запрос заметно преобразится.

Команды UPDATE также могут использоваться для атаки. Опять же, есть угроза разделения инструкции на несколько частей и присоединения дополнительного запроса. Также взломщик может видоизменить выражение SET. В этом случае потенциальному взломщику необходимо обладать некоторой дополнительной информацией о структуре базы данных для успешного манипулирования запросами. Эту информацию можно получить, проанализировав используемые в форме имена переменных, либо просто перебирая все наиболее распространенные варианты названия соответствующих полей (а их не так уж и много).

Пример #3 От восстановления пароля... до получения дополнительных привилегий (для любой базы данных)

<?php
$query 
"UPDATE usertable SET pwd='$pwd' WHERE uid='$uid';";
?>
Но злоумышленник может ввести значение ' or uid like'%admin%' для переменной $uid для изменения пароля администратора или просто присвоить переменной $pwd значение hehehe', trusted=100, admin='yes для получения дополнительных привилегий. При выполнении запросы переплетаются:
<?php

// $uid: ' or uid like '%admin%
$query "UPDATE usertable SET pwd='...' WHERE uid='' or uid like '%admin%';";

// $pwd: hehehe', trusted=100, admin='yes
$query "UPDATE usertable SET pwd='hehehe', trusted=100, admin='yes' WHERE
...;"
;

?>

Пугающий пример того, как на сервере баз данных могут выполняться команды операционной системы.

Пример #4 Выполнение команд операционной системы на сервере (для базы MSSQL)

<?php

$query  
"SELECT * FROM products WHERE id LIKE '%$prod%'";
$result mssql_query($query);

?>
Если взломщик введет значение a%' exec master..xp_cmdshell 'net user test testpass /ADD' -- для переменной $prod, тогда запрос $query будет выглядеть так:
<?php

$query  
"SELECT * FROM products
           WHERE id LIKE '%a%'
           exec master..xp_cmdshell 'net user test testpass /ADD' --%'"
;
$result mssql_query($query);

?>
MSSQL сервер выполняет SQL-команды в пакетном режиме, в том числе и операции по заведению локальных учетных записей базы данных. В случае, если приложение работает с привилегиями администратора sa и сервис MSSQL запущен с необходимыми привилегиями, то выполнив приведенные выше действия, взломщик получит аккаунт для доступа к серверу.

Замечание:

Некоторые приведенные в этой главе примеры касаются конкретной базы данных. Это не означает, что аналогичные атаки на другие программные продукты невозможны. Работоспособность вашей базы данных может быть нарушена каким-либо другим способом.

Рабочий пример проблем, вызываемых SQL-инъекцией
Авторство изображения принадлежит » xkcd

Способы защиты

Хотя по-прежнему очевидно, что взломщик должен обладать по крайней мере некоторыми знаниями о структуре базы данных чтобы провести успешную атаку, получить эту информацию зачастую очень просто. Например, если база данных является частью open-source или другого публично доступного программного пакета с инсталляцией по умолчанию, эта информация является полностью открытой и доступной. Эти данные также могут быть получены из закрытого проекта, даже если он закодирован, усложнен, или скомпилирован, и даже из вашего личного кода через отображение сообщений об ошибках. К другим методам относится использование распространенных (легко угадываемых) названий таблиц и столбцов. Например, форма логина, которая использует таблицу 'users' c названиями столбцов 'id', 'username' и 'password'.

Большинство успешных атак основывается на коде, написанном без учета соответствующих требований безопасности. Не доверяйте никаким вводимым данным, особенно если они поступают со стороны клиента, даже если это списки в форме, скрытые поля или куки. Первый приведенный пример показывают, как подобные запросы могут привести к катастрофе.

  • Никогда не соединяйтесь с базой данных, используя учетную запись владельца базы данных или суперпользователя. Всегда старайтесь использовать специально созданных пользователей с максимально ограниченными правами.
  • используйте подготовленные выражения с привязанными переменными. Эта возможность предоставляется расширениями PDO, MySQLi и другими библиотеками.
  • Всегда проверяйте введенные данные на соответствие ожидаемому типу. В PHP есть множество функций для проверки данных: начиная от простейших функций для работы с переменными и функций определения типа символов (таких как is_numeric() и ctype_digit() соответственно) и заканчивая Perl-совместимыми регулярными выражениями.
  • В случае, если приложение ожидает цифровой ввод, примените функцию ctype_digit() для проверки введенных данных, или принудительно укажите их тип при помощи settype(), или просто используйте числовое представление при помощи функции sprintf().

    Пример #5 Более безопасная реализация постраничной навигации

    <?php

    settype
    ($offset'integer');
    $query "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";

    // обратите внимание на формат %d, использование %s было бы бессмысленно
    $query sprintf("SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET %d;",
                     
    $offset);

    ?>

  • Если на уровне базы данных не поддерживаются привязанные переменные, то всегда экранируйте любые нечисловые данные, используемый в запросах к БД при помощи специальных экранирующих функций, специфичных для используемой вами базы данных (например, mysql_real_escape_string(), sqlite_escape_string() и т.д.). Общие функции такие как addslashes() полезны только в определенных случаях (например MySQL в однобайтной кодировке с отключенным NO_BACKSLASH_ESCAPES), поэтому лучше избегать их использование.
  • Ни в коем случае не выводите никакой информации о БД, особенно о ее структуре. Также ознакомьтесь с соответствующими разделами документации: "Сообщения об ошибках" и "Функции обработки и логирования ошибок".
  • Вы можете использовать хранимые процедуры и заранее определенные курсоры для абстрагированной работы с данными, не предоставляя пользователям прямого доступа к данным и представлениям, но это решение имеет свои особенности.

Помимо всего вышесказанного, вы можете логировать запросы в вашем скрипте либо на уровне базы данных, если она это поддерживает. Очевидно, что логирование не может предотвратить нанесение ущерба, но может помочь при трассировке взломанного приложения. Лог-файл полезен не сам по себе, а информацией, которая в нем содержится. Причем, в большинстве случаев полезно логировать все возможные детали.