Что такое SQL-инъекция и как от неё защититься?
- Почему нельзя собирать SQL конкатенацией строк?
- Как работают параметризованные запросы?
- Защищает ли ORM от SQL-инъекций?
Инъекция — данные пользователя попадают в текст запроса и меняют его смысл: ' OR '1'='1 превращает проверку пароля в «всегда истина».
- Защита — параметризованные запросы: текст запроса и данные передаются в базу отдельно
- ORM защищает, пока не собирать сырой SQL из строк
- Имена колонок и направление сортировки параметром не передать — только белый список
А ещё Плюс: минимальные права пользователя базы, не показывать ошибки СУБД наружу
Подробно
Если запрос собирается конкатенацией:
"SELECT * FROM users WHERE login = '" + login + "' AND password = '" + pass + "'"
то ввод admin' -- превращает остаток в комментарий, а x' OR '1'='1 делает условие истинным. Дальше хуже: UNION SELECT вытаскивает данные из других таблиц, в некоторых СУБД можно выполнить несколько команд (; DROP TABLE), прочитать файлы или выполнить команды ОС. Даже без видимого вывода работает «слепая» инъекция — по времени ответа (pg_sleep) или по разнице в поведении.
Главная защита — параметризованные запросы (prepared statements):
SELECT * FROM users WHERE login = ? AND password_hash = ?
Текст запроса и значения уходят в базу раздельно; значение никогда не разбирается как SQL, сколько бы кавычек в нём ни было. Поддержано везде: PreparedStatement в JDBC, плейсхолдеры в psycopg и SQLAlchemy, именованные параметры в JPA.
ORM использует параметры, но уязвимости возвращаются через сырой SQL: createNativeQuery("... " + filter), text(f"... {x}"), конкатенация в JPQL, extra() и raw() в Django.
Что нельзя передать параметром: имена таблиц и колонок, направление сортировки, ключевые слова. Для ORDER BY по выбору пользователя — белый список: сопоставить ввод с допустимыми колонками и подставить только известное значение. Для IN (...) — сгенерировать нужное число плейсхолдеров.
Дополнительные слои:
- пользователь базы с минимальными правами (приложению не нужен
DROPи доступ к чужим схемам); - не показывать текст ошибок СУБД пользователю;
- валидация формата (id — число) — полезна, но не замена параметрам;
- WAF как страховка, не как основная защита.
Тот же класс ошибок — любые инъекции в интерпретаторы: команды ОС (subprocess с shell=True), LDAP, NoSQL (операторы $ne в MongoDB из JSON), шаблоны, а теперь и промпты языковых моделей.
Пример
# уязвимо
cur.execute(f"SELECT * FROM orders WHERE id = {order_id}")
# безопасно: значение передаётся отдельно
cur.execute("SELECT * FROM orders WHERE id = %s", (order_id,))
# сортировка — белый список
SORT = {"date": "created_at", "sum": "total"}
cur.execute(f"SELECT * FROM orders ORDER BY {SORT.get(sort, 'created_at')} DESC")Что хотят услышать
- Механика: данные меняют структуру запроса, пример с кавычкой.
- Параметризованные запросы как основная защита и почему они работают.
- ORM не панацея: сырой SQL, нативные запросы.
- Белый список для идентификаторов, минимальные права.
Частые ошибки
- Экранировать кавычки вручную вместо параметров.
- Думать, что использование ORM само по себе исключает инъекции.
Проверьте себя
Один верный вариант. Подумайте, потом откройте ответ.
Показать варианты и ответ
- ✓Параметризованные запросы: текст запроса и данные передаются отдельно; имена колонок — только из белого списка
- ✗Экранировать кавычки во вводе пользователя вручную или через ORM, а имена колонок и направление сортировки передавать параметрами запроса
- ✗ORM защищает даже при сборке сырого SQL из строк
- ✗Направление сортировки можно передать параметром запроса
Связанные вопросы
Разбор собеседования с sotto
После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.