SQL-инъекция: как работает и как от неё защищаться
SQL-инъекция известна больше двадцати лет, защита от неё занимает одну строчку кода, и она всё равно регулярно оказывается причиной утечек. Причина не в сложности, а в том, что уязвимый вариант часто короче и удобнее правильного.
Разберём механику и защиту.
Как это работает
Приложение собирает запрос из строк, подставляя туда пользовательский ввод:
query = "SELECT * FROM users WHERE email = '" + email + "'"
Пользователь вводит обычную почту, и всё в порядке. Но если он введёт строку с кавычкой и своим куском SQL, она попадёт прямо в текст запроса и станет его частью.
База не различает, где ваш код, а где данные: она получает одну строку и выполняет её целиком. Именно в этом суть уязвимости, а не в конкретных символах.
Последствия зависят от прав пользователя базы и фантазии атакующего: чтение чужих данных, обход авторизации, изменение или удаление записей, в тяжёлых случаях выполнение команд на сервере.
Почему до сих пор встречается
Уязвимый код проще написать. Строку склеить быстрее, чем разбираться с параметрами, особенно когда запрос собирается динамически из условий.
Плюс типичные места, где защита забывается:
Динамическая сортировка: имя колонки в ORDER BY нельзя передать параметром, и разработчик подставляет его строкой.
Поиск с LIKE, где к вводу добавляются проценты.
Списки идентификаторов в IN, которые собираются склейкой.
Запросы в отчётах и админках, которые «внутренние, туда никто чужой не зайдёт».
Последний пункт особенно опасен: внутренние инструменты живут годами и однажды оказываются доступны шире, чем предполагалось.
Правильная защита
Параметризованные запросы. Главный и почти единственный по-настоящему надёжный способ. Вы отдаёте базе шаблон запроса отдельно и значения отдельно, и база гарантированно трактует значения как данные, а не как код.
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
Разница с первым примером в одну запятую, а по смыслу принципиальная.
ORM. Современные ORM по умолчанию используют параметры, поэтому обычный код через них безопасен. Но дыра появляется там, где вы вставляете сырой SQL, а такие места есть почти в каждом проекте.
Белые списки там, где параметры невозможны. Для сортировки и имён таблиц параметр не подходит, поэтому проверяйте значение по заранее известному списку допустимых. Не экранируйте, а сверяйте с перечнем.
Права доступа. Приложение не должно ходить в базу под суперпользователем. Отдельная роль с минимальными правами превращает потенциальную катастрофу в неприятность.
Чего делать не надо: писать свою функцию экранирования кавычек. Кодировки, юникод и особенности диалектов делают этот путь минным полем, а выигрыш нулевой.
Слепые инъекции
Отдельная разновидность, которую стоит знать. Приложение не показывает ошибку и не возвращает данные напрямую, но по-разному себя ведёт: отвечает быстрее или медленнее, показывает разный текст.
Атакующий задаёт базе вопросы вида «первый символ пароля больше M?» и по времени ответа читает данные посимвольно. Медленно, но работает, а автоматические инструменты делают это за минуты.
Практический вывод: отсутствие видимой ошибки не означает защищённость. Сообщения об ошибках прятать надо, но это не защита от инъекции, а гигиена.
Как проверить свой проект
Поиск по кодовой базе на конкатенацию строк рядом с SQL. Обычно находится быстро.
Проверка мест с сырыми запросами в ORM.
Просмотр всего, что собирается динамически: фильтры, сортировки, отчёты.
Тестовые запросы с кавычкой в каждом поле ввода. Примитивно, но неожиданно результативно.
Статический анализатор в пайплайне: большинство ловит очевидные случаи.
Что спрашивают на собеседовании
Что такое SQL-инъекция и как от неё защититься. Почему параметризованные запросы безопасны. Достаточно ли ORM. Что делать с динамической сортировкой. Почему нельзя просто экранировать кавычки.
Дальше обычно расширяют на общую безопасность: XSS, CSRF, хранение паролей, работа с секретами. Для бэкендера и тестировщика это стандартный блок, и на нём часто сыпятся, потому что готовятся к алгоритмам, а не к этому.
Про то, как устроены запросы в целом, писали в статье о реляционных базах данных, а про интерфейсы приложения в разборе что такое API.
Коротко
Не склеивайте SQL из строк. Используйте параметры. Для имён колонок применяйте белые списки. Ограничивайте права пользователя базы. Не пишите своё экранирование.
Пять правил, и целый класс уязвимостей закрыт.
Проверить, что тема уложилась, можно вопросами: в Сеньорчике есть блок по веб-безопасности в треках бэкенда и тестирования, с разбором каждого вопроса. Движок возвращает темы, где вы ошибаетесь. Десять минут в день в Telegram, бесплатно.