PHP 8.3.4 Released!

SQL-инъекции

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

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

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

<?php

$offset
= $_GET['offset']; // осторожно, нет валидации ввода!
$query = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";
$result = pg_query($conn, $query);
?>
Обычно пользователи нажимают по ссылкам 'вперёд' и 'назад', вследствие чего значение переменной $offset заносится в URL. Скрипт ожидает, что $offset - десятичное число. Однако, взломщик может попытаться взломать систему, присоединив к URL следующее значение:
0;
insert into pg_shadow(usename,usesysid,usesuper,usecatupd,passwd)
  select 'crack', usesysid, 't','t','crack'
  from pg_shadow where usename='postgres';
--
Если это произойдёт, скрипт предоставит злоумышленнику доступ суперпользователя. Обратите внимание, что значение 0; использовано для того, чтобы задать правильное смещение для первого запроса и корректно его завершить.

Замечание:

Это распространённый приём, чтобы заставить синтаксический анализатор SQL игнорировать остальную часть запроса, написанного разработчиком с помощью --, который является знаком комментария в 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;
--

Выражения UPDATE и INSERT также подвержены таким атакам.

Пример #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
...;"
;
?>

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

Пример #4 Атака на операционную систему сервера базы данных (MSSQL Server)

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

<?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 Server выполняет SQL запросы в пакете, включая команду добавления нового пользователя в локальную базу данных учётных записей. Если бы это приложение было запущено от имени sa и служба MSSQLSERVER была запущена с достаточными привилегиями, у злоумышленника появилась бы учётная запись, с помощью которой он мог бы получить доступ к этой машине.

Замечание:

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

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

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

Рекомендуемый способ избежать SQL-инъекций - связывание всех данных с помощью подготовленных запросов. Использование подготовленных запросов недостаточно для полного предотвращения SQL-инъекций, но это самый простой и безопасный способ обеспечить ввод данных в SQL-запросы. Все динамические литералы данных в выражениях WHERE, SET и VALUES должны быть заменены заполнителями. Фактические данные будут связаны во время выполнения и отправлены отдельно от команды SQL.

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

Пример #5 Избегание SQL-инъекций с помощью подготовленных операторов PDO

<?php

// Динамическая часть SQL проверяется на соответствие ожидаемым значениям
$sortingOrder = $_GET['sortingOrder'] === 'DESC' ? 'DESC' : 'ASC';
$productId = $_GET['productId'];

// SQL подготавливается с заполнителем
$stmt = $pdo->prepare("SELECT * FROM products WHERE id LIKE ? ORDER BY price {$sortingOrder}");

// Значение предоставляется с подстановочными знаками LIKE
$stmt->execute(["%{$productId}%"]);
?>

Подготовленные операторы предоставляются PDO, MySQLi, а также другими библиотеками баз данных.

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

Стратегия "защита в глубину" включает в себя несколько эффективных методов написания кода:

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

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

add a note

User Contributed Notes 1 note

up
39
Richard dot Corfield at gmail dot com
12 years ago
The best way has got to be parameterised queries. Then it doesn't matter what the user types in the data goes to the database as a value.

A quick search online shows some possibilities in PHP which is great! Even on this site - http://php.net/manual/en/pdo.prepared-statements.php
which also gives the reasons this is good both for security and performance.
To Top