SQL-инъекция (как защищаться)
| Тип | инъекция кода (CWE-89) |
|---|---|
| Категория OWASP | A03:2021 Injection |
| Причина | склейка SQL-запроса из строк с пользовательскими данными |
| Главная защита | параметризованные (подготовленные) запросы |
| Дополнительно | белые списки для имён, ORM, минимальные права, WAF |
| Инструменты проверки | sqlmap, Semgrep, Bandit |
SQL-инъекция (англ. SQL injection, SQLi) — уязвимость, при которой данные, пришедшие от пользователя, попадают в текст SQL-запроса и интерпретируются базой данных как часть команды.[2] Атакующий может прочитать чужие данные, обойти проверку пароля, изменить или удалить таблицы, а в некоторых СУБД — выполнить команды операционной системы. Инъекции десятилетиями входят в OWASP Top 10 (категория A03:2021).[3]
Как возникает
Причина всегда одна — запрос собирается склейкой строк:
# УЯЗВИМО
name = request.args["name"]
cur.execute(f"SELECT * FROM users WHERE name = '{name}'")
Если передать name = ' OR '1'='1, запрос станет
SELECT * FROM users WHERE name = '' OR '1'='1'
и вернёт всех пользователей. Значение '; DROP TABLE users; -- в драйверах, допускающих несколько команд в одном вызове, удалит таблицу. На практике чаще встречаются более тонкие варианты:
- UNION-инъекция — к исходному запросу дописывается
UNION SELECTиз другой таблицы, например с хешами паролей. - Слепая инъекция — результат не виден, но атакующий задаёт вопросы «да/нет» и наблюдает за разницей в ответе или временем (
pg_sleep(5)). - Инъекция второго порядка — значение сначала безопасно сохраняется в базу, а потом другой код достаёт его и склеивает в новый запрос.
- Инъекции в
ORDER BY, имена столбцов и таблиц,LIMIT— там, где обычные параметры не работают.
Главная защита: параметризованные запросы
В параметризованном (подготовленном, prepared) запросе текст SQL и данные передаются в СУБД раздельно. База сначала разбирает запрос с заполнителями, а значения подставляет как данные — они никогда не станут кодом, какие бы кавычки в них ни были.[1]
# Python, psycopg (PostgreSQL)
cur.execute("SELECT * FROM users WHERE name = %s", (name,))
# sqlite3
cur.execute("SELECT * FROM users WHERE name = ?", (name,))
// Go, database/sql
row := db.QueryRowContext(ctx, "SELECT id FROM users WHERE name = $1", name)
Стиль заполнителей зависит от драйвера (?, %s, $1, :name — их перечисляет PEP 249[4]), но принцип один: никогда не форматировать SQL строковыми операциями — ни f-строками, ни %, ни +, ни .format(). Важно: %s в psycopg — это заполнитель драйвера, а не форматирование Python; значение передаётся вторым аргументом.[5]
Когда параметры не помогают
Заполнителем можно заменить только значение, но не идентификатор (имя таблицы или столбца) и не ключевое слово (ASC/DESC). Для таких мест:
- Белый список: сопоставить ввод с заранее известными вариантами.
SORT = {"name": "name", "date": "created_at"}
col = SORT.get(request.args.get("sort"), "created_at")
order = "DESC" if request.args.get("dir") == "desc" else "ASC"
cur.execute(f"SELECT * FROM posts ORDER BY {col} {order} LIMIT %s", (limit,))
- Если нужен динамический идентификатор, использовать безопасный построитель драйвера, например
psycopg.sql.Identifier, который корректно экранирует имя.
Ручное экранирование кавычек (replace("'", "''")) — ненадёжная и устаревшая защита: легко забыть место, ошибиться с кодировкой или контекстом.
ORM и построители запросов
ORM (SQLAlchemy, Django ORM, GORM, Prisma) параметризуют запросы автоматически, поэтому обычный код через них безопасен. Уязвимости возвращаются, когда разработчик спускается к «сырому» SQL: text(f"..."), .raw(), .extra(), whereRaw со склейкой строк. Правило то же — сырые запросы только с параметрами.
Эшелонированная защита
Параметризация устраняет причину; остальные меры уменьшают ущерб, если где-то ошибка всё же осталась:
- Минимальные права учётной записи приложения в БД: только нужные таблицы и операции, без
DROP, без прав суперпользователя и доступа к файловой системе (см. Принцип наименьших привилегий). - Запрет нескольких команд в одном вызове на уровне драйвера.
- Скрытие подробных ошибок СУБД от пользователя — они сильно облегчают слепые инъекции.
- Валидация типов на входе (число — это число, UUID — это UUID).
- Статический анализ: Semgrep, Bandit (правило B608), CodeQL находят склейку строк в
execute. - Тестирование: sqlmap автоматически ищет и эксплуатирует инъекции — применять только к своим системам.[6]
- WAF отсекает типовые полезные нагрузки, но легко обходится и не заменяет исправления кода.
Родственные уязвимости
Тот же класс ошибок — смешение кода и данных — встречается в командах ОС (shell injection), шаблонизаторах (SSTI), запросах к NoSQL-базам (операторы $where, $ne в MongoDB), LDAP и XPath. В приложениях с языковыми моделями аналогом стала prompt injection, у которой, в отличие от SQL, нет полного решения вроде параметризации.