flawopen.com/SQL-инъекция/Python

SQL-инъекция в Python

Критическая CWE-89 Черновик — ожидает проверки
Language: English Deutsch Español Français हिन्दी Bahasa Indonesia 日本語 한국어 Português (Brasil) Русский 简体中文
Простыми словами

Представьте форму, которая ожидает только номер билета, например 482. SQL-инъекция происходит, когда кто-то вводит в это поле не число, а хитрую фразу, из-за которой система отвечает «покажи все билеты» вместо просто билета 482 — потому что система так и не проверила, что получила действительно только число.

Ключевые термины на этой странице
данные, контролируемые пользователем
Любое значение, которое в конечном счёте пришло от того, кто использует приложение — или атакует его: поле формы, параметр URL, имя загруженного файла, HTTP-заголовок. Приложение не может считать его заведомо корректным или безопасным.
SQL-запрос
Команда, отправляемая базе данных — например, «получить эту строку», «удалить эту таблицу». Её смысл целиком определяется точным текстом, из-за чего внедрение постороннего текста в него так опасно.

Что происходит

SQL-инъекция происходит, когда данные, контролируемые пользователем, вставляются напрямую в текст запроса к базе данных вместо того, чтобы передаваться как отдельное значение. Если код собирает запрос путём склеивания строк, атакующий может передать данные, которые изменят реальную структуру запроса — превратив запрос «получить одну строку» в запрос, возвращающий все строки, или удаляющий таблицу.

В Python это почти всегда выглядит одинаково: обращение к базе данных строится с помощью f-строки, форматирования через % или конкатенации через + вместо встроенных в драйвер базы данных плейсхолдеров параметров.

Реальные последствия

В 2015 году британский телекоммуникационный оператор TalkTalk пострадал от утечки данных более 150 000 клиентов после того, как злоумышленники воспользовались уязвимостью SQL-инъекции на устаревшей веб-странице, доставшейся компании в результате поглощения. Регулятор по защите данных Великобритании оштрафовал TalkTalk на £400 000, назвав эту ошибку предотвратимой и элементарной.

Источник: постановление Information Commissioner's Office Великобритании, 2016 — см. раздел «Источники» ниже.

Уязвимый вариант и исправленный

УЯЗВИМО
# user_id приходит прямо из запроса
def get_user(cursor, user_id):
    query = f"SELECT * FROM users WHERE id = {user_id}"
    cursor.execute(query)
    return cursor.fetchone()
ИСПРАВЛЕНО
# значение передаётся отдельно, никогда не встраивается
def get_user(cursor, user_id):
    query = "SELECT * FROM users WHERE id = %s"
    cursor.execute(query, (user_id,))
    return cursor.fetchone()

Почему это исправление работает

Исправленная версия передаёт текст запроса и значение в execute() как два отдельных аргумента. Драйвер базы данных также отправляет их в базу отдельно — структура запроса фиксируется до того, как к ней присоединяется значение, поэтому значение никогда не может быть интерпретировано как часть синтаксиса SQL, какие бы символы оно ни содержало. F-строка так не умеет: к моменту, когда execute() получает запрос, значение уже встроено в текст так, будто всегда было частью команды.

Особенности Python

Форматирование через % внутри execute() всё ещё выглядит как параметризация — но это не так

cursor.execute("... WHERE id = %s" % user_id) так же уязвимо, как и f-строка. Плейсхолдер становится безопасным только тогда, когда значение передаётся как отдельный второй аргумент самого execute()cursor.execute("...WHERE id = %s", (user_id,)) — чтобы подстановку выполнял драйвер, а не форматирование строк Python.

ORM параметризуют запросы по умолчанию, но не их «аварийные выходы»

ORM Django и построитель запросов SQLAlchemy автоматически параметризуют обычные запросы. Риск возвращается, как только вы используете Model.objects.raw() или text() из SQLAlchemy и собираете этот «сырой» SQL с помощью f-строки.

Синтаксис плейсхолдеров различается между драйверами

psycopg2 (PostgreSQL) использует %s независимо от типа столбца; sqlite3 использует ?. Копирование стиля плейсхолдера из документации одного драйвера в другой приводит к тихой поломке — проверяйте стиль параметров именно вашего драйвера, а не полагайтесь на предположения.

Распространённые заблуждения

«Мой ORM защищает меня автоматически»

Верно для обычного API запросов ORM — неверно, как только вы используете raw() или text() и собираете эту строку самостоятельно.

«Этот ID всегда число, значит, интерполировать его безопасно»

Риск не в типе значения во время выполнения — он в том, что запрос вообще строится через интерполяцию строк. В тот момент, когда это предположение перестанет выполняться в любой точке жизненного цикла кода, уязвимость уже будет на месте.

«Я сам экранирую кавычки, значит, параметризованные запросы не нужны»

Ручное экранирование зависит от драйвера, и его легко сделать неправильно в неочевидных местах. Параметризованные запросы — это не более строгая форма экранирования, они полностью устраняют проблему, поскольку значение вообще никогда не становится частью текста запроса.

Как проверить, затронуты ли вы

grep -rn "execute(f\"" --include="*.py" . grep -rn "execute(.*%\s*(" --include="*.py" . grep -rn "\.raw(\|text(" --include="*.py" .
Лучше, чем просто grep: запустите Bandit (правило B608, hardcoded_sql_expressions) в CI — он автоматически обнаруживает этот паттерн и приводит к падению сборки при новом случае.

Чек-лист профилактики

Частые вопросы

Защищает ли использование ORM от SQL-инъекций?

Для его обычных методов запросов — да. «Аварийные выходы» для сырых запросов — нет: они настолько же безопасны, насколько и SQL, написанный вручную, не более.

Этот риск касается только полей поиска?

Нет. Учитывается всё, что фактически контролируется внешней стороной — HTTP-заголовки, имена загруженных файлов, даже значение из стороннего API, которому доверяет приложение.

Можно ли просто экранировать кавычки самому?

Можно, но это хрупко и зависит от драйвера. Параметризованные запросы — это настоящее решение, а не более строгая форма экранирования.

Источники

Смотреть на: Python JavaScriptGoJava PHPC#Ruby C/C++RustKotlin Swift Solidity (N/A)
Смотрите также: Command InjectionPath Traversal XSSНебезопасная десериализация