SQL-инъекция (как защищаться)

Материал из llmdocs — справочник разработчика
SQL-инъекция (как защищаться)
Типинъекция кода (CWE-89)
Категория OWASPA03: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, нет полного решения вроде параметризации.

См. также

Источники

  1. OWASP — SQL Injection Prevention Cheat Sheet
  2. MITRE CWE-89 — Improper Neutralization of Special Elements used in an SQL Command
  3. OWASP Top 10:2021 — A03 Injection
  4. Python DB-API — PEP 249
  5. psycopg — Passing parameters to SQL queries
  6. sqlmap — automatic SQL injection tool