Введение
SQL-инъекция — одна из самых известных и опасных уязвимостей веб/desktop-приложений, работающих с БД, упоминавшаяся вскользь в статье 425 (QSqlQuery::bindValue()) — в этой статье разберём механизм атаки подробно, и единственный надёжный способ защиты — параметризованные запросы, продолжая тему безопасности из криптографического цикла (статьи 371-380).
Концепция
SQL-инъекция возникает, когда пользовательский ввод напрямую, без должной обработки, встраивается в текст SQL-запроса в виде строки — злоумышленник, контролирующий этот ввод, может включить в него фрагменты SQL-синтаксиса, изменяющие логику исходного запроса непредсказуемым (с точки зрения разработчика) образом, вплоть до полного обхода предполагаемой логики аутентификации или прямого доступа к произвольным данным БД. Параметризованные запросы (prepared statements) решают эту проблему фундаментально, а не просто «снижают риск» — значения параметров передаются в СУБД отдельно от текста самого запроса, и СУБД гарантированно обрабатывает их именно как литеральные значения данных, а не как часть исполняемого SQL-синтаксиса, независимо от их содержимого.
Пример кода: SQL-инъекции
// УЯЗВИМЫЙ код — прямая конкатенация пользовательского ввода в текст запроса
// НЕ ДЕЛАЙТЕ ТАК — приведено исключительно для иллюстрации механизма атаки
QString username = getUserInput(); // предположим, пользователь ввёл: ' OR '1'='1
QString query = "SELECT * FROM users WHERE username = '" + username + "'";
// Итоговый запрос: SELECT * FROM users WHERE username = '' OR '1'='1'
// Условие "'1'='1'" истинно ВСЕГДА — запрос вернёт ВСЕХ пользователей, а не одного конкретного!
QSqlQuery query_obj;
query_obj.exec(query); // выполнение этого скомпрометированного, "склеенного" запроса
// БЕЗОПАСНЫЙ код — параметризованный запрос через QSqlQuery::prepare()/bindValue()
QString username = getUserInput(); // тот же потенциально злонамеренный ввод
QSqlQuery query;
query.prepare("SELECT * FROM users WHERE username = :username");
query.bindValue(":username", username);
query.exec();
// Независимо от содержимого username (включая "' OR '1'='1"), СУБД обработает
// его строго как ОДНО, буквальное значение для сравнения, а не как часть SQL-синтаксиса —
// запрос ищет пользователя с буквальным именем "' OR '1'='1", которого, скорее всего, не существует
-- Более разрушительный пример классической инъекции — не просто обход условия,
-- а возможность выполнить ПРОИЗВОЛЬНУЮ дополнительную команду через ';'
-- Пользовательский ввод: '; DROP TABLE users; --
-- При прямой конкатенации это превратилось бы в ДВЕ отдельные команды:
-- SELECT * FROM users WHERE username = ''; DROP TABLE users; --'
-- В зависимости от конкретного API выполнения запроса, это может фактически удалить ВСЮ таблицу users
Пояснения к коду
Уязвимый пример показывает классический, простейший вид инъекции — специально сконструированный пользовательский ввод (' OR '1'='1) «вырывается» из предполагаемых разработчиком кавычек строкового литерала и добавляет собственное, всегда истинное условие, делая фильтрацию по конкретному имени пользователя бессмысленной. Безопасная версия через prepare()/bindValue() решает проблему не через попытку «очистки» или «экранирования» пользовательского ввода вручную (что исторически было распространённым, но менее надёжным подходом, легко допускающим упущенные случаи), а через структурную, архитектурную защиту — СУБД заранее знает структуру запроса (с плейсхолдером :username) до получения конкретного значения, и при последующей привязке значения через bindValue() физически не интерпретирует это значение как код, независимо от его содержимого. Пример с '; DROP TABLE users; демонстрирует, что инъекция — не только риск утечки/обхода фильтрации данных, но и потенциальная возможность выполнения произвольных, разрушительных команд, если используемый API допускает выполнение нескольких команд через точку с запятой в одном вызове.
Подводные камни
- Попытка защититься от инъекций через ручное экранирование специальных символов (замену одиночных кавычек на удвоенные, или аналогичные приёмы) вместо использования параметризованных запросов — этот подход исторически использовался, но крайне ненадёжен: легко упустить какой-либо специальный символ или особый случай кодировки, специфичный для конкретной СУБД, и параметризованные запросы (доступные практически во всех современных API работы с БД, включая
QSqlQuery) полностью устраняют эту категорию риска структурно, а не путём попыток «угадать и обработать» все возможные опасные входные данные вручную. - Применение параметризации только для значений, явно введённых пользователем через форму, но не для значений, формируемых программно из других, казалось бы доверенных источников (например, значение, полученное из внешнего API, файла конфигурации, или другой таблицы той же БД) — любые данные, не являющиеся буквальным, статическим литералом, написанным непосредственно разработчиком в исходном коде запроса, потенциально могут содержать неожиданные символы (намеренно или случайно) и должны передаваться через параметризацию, а не через конкатенацию строк, независимо от того, «кажется» ли конкретный источник данных доверенным.
- Использование параметризации для значений данных, но не для имён таблиц/столбцов, которые по самой природе параметризованных запросов (предназначенных для значений, а не для структурных элементов SQL) не могут быть параметризованы тем же способом — для случаев, где имя таблицы или столбца действительно должно определяться динамически (что само по себе нетипичная, требующая осторожности ситуация), нужна отдельная, явная проверка/валидация против заранее известного, ограниченного списка допустимых значений (allowlist), а не прямая конкатенация полученного откуда-либо имени без проверки.
- Ложное чувство полной защищённости от любых атак исключительно благодаря использованию параметризованных запросов, без учёта других, отдельных категорий уязвимостей (XSS при отображении данных, полученных из БД, в веб-интерфейсе; уязвимости авторизации, позволяющие легитимному, аутентифицированному пользователю получить доступ к данным, не предназначенным для него, даже при полностью безопасном с точки зрения инъекций SQL-коде) — параметризация решает именно и только проблему SQL-инъекций, оставляя актуальными множество других аспектов безопасности приложения, разбираемых в других частях общего цикла статей про безопасность.