flawopen.com/SQL-инъекция/Python
Представьте форму, которая ожидает только номер билета, например 482. SQL-инъекция происходит, когда кто-то вводит в это поле не число, а хитрую фразу, из-за которой система отвечает «покажи все билеты» вместо просто билета 482 — потому что система так и не проверила, что получила действительно только число.
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() получает запрос, значение уже встроено в текст так, будто всегда было частью команды.
cursor.execute("... WHERE id = %s" % user_id) так же уязвимо, как и f-строка. Плейсхолдер становится безопасным только тогда, когда значение передаётся как отдельный второй аргумент самого execute() — cursor.execute("...WHERE id = %s", (user_id,)) — чтобы подстановку выполнял драйвер, а не форматирование строк Python.
ORM Django и построитель запросов SQLAlchemy автоматически параметризуют обычные запросы. Риск возвращается, как только вы используете Model.objects.raw() или text() из SQLAlchemy и собираете этот «сырой» SQL с помощью f-строки.
psycopg2 (PostgreSQL) использует %s независимо от типа столбца; sqlite3 использует ?. Копирование стиля плейсхолдера из документации одного драйвера в другой приводит к тихой поломке — проверяйте стиль параметров именно вашего драйвера, а не полагайтесь на предположения.
Верно для обычного API запросов ORM — неверно, как только вы используете raw() или text() и собираете эту строку самостоятельно.
Риск не в типе значения во время выполнения — он в том, что запрос вообще строится через интерполяцию строк. В тот момент, когда это предположение перестанет выполняться в любой точке жизненного цикла кода, уязвимость уже будет на месте.
Ручное экранирование зависит от драйвера, и его легко сделать неправильно в неочевидных местах. Параметризованные запросы — это не более строгая форма экранирования, они полностью устраняют проблему, поскольку значение вообще никогда не становится частью текста запроса.
grep -rn "execute(f\"" --include="*.py" .
grep -rn "execute(.*%\s*(" --include="*.py" .
grep -rn "\.raw(\|text(" --include="*.py" .
Для его обычных методов запросов — да. «Аварийные выходы» для сырых запросов — нет: они настолько же безопасны, насколько и SQL, написанный вручную, не более.
Нет. Учитывается всё, что фактически контролируется внешней стороной — HTTP-заголовки, имена загруженных файлов, даже значение из стороннего API, которому доверяет приложение.
Можно, но это хрупко и зависит от драйвера. Параметризованные запросы — это настоящее решение, а не более строгая форма экранирования.