Предотвращение SQL-инъекций: точка зрения автора Django

Это пост, соавтором которого является Джейкоб Каплан-Мосс, соавтор Django и Грейсон Хардуэй.

Что такое SQL Injection?

SQL Injection (SQLi) - один из самых опасных классов веб-уязвимостей. К счастью, она становится все более редкой - в основном благодаря растущему использованию уровней абстракции баз данных, таких как ORM в Django, - но в тех случаях, когда она происходит, она может быть разрушительной.

SQLi возникает, когда код неправильно строит SQL-запросы, содержащие пользовательский ввод. Например, представьте, что вы пишете функцию поиска, не зная о SQLi:

def search(request):
    query = request.GET['q']
    sql = f"SELECT * FROM some_table WHERE title LIKE '%{query}%';"

    cursor = db.cursor()
    cursor.execute(sql)
    ...

Можете ли вы определить проблему? Обратите внимание, что запрос поступает из браузера: request.GET['q']. Подумайте о том, что может произойти, если этот запрос содержит одинарную кавычку. Что происходит, когда создается строка SQL?

Представьте, что злоумышленник ищет ' OR 'a'='a. В этом случае построенный SQL будет выглядеть так:

SELECT * FROM some_table WHERE title LIKE '%%' OR 'a'='a';

Так что это плохо; теперь мы возвращаем все содержимое таблицы. Это может стать утечкой данных или перегрузить ваш сервер базы данных.

Но все еще хуже; представьте, что злоумышленник ищет '; DELETE FROM some_table. Теперь построенный SQL становится таким:

SELECT * FROM some_table WHERE title LIKE '%%'; DELETE FROM some_table;

Ух ох.

Общие концепции для предотвращения SQLi

Вскоре мы перейдем к специфике Django, но сначала важно понять фундаментальные правила предотвращения SQL-инъекций:

  1. Никогда не доверяйте никаким данным, предоставленным пользователем
  2. Всегда используйте "параметризованные операторы" при непосредственном построении SQL-запросов

Все, что исходит от пользователя, может быть создано злонамеренно. Даже вещи, которые кажутся безопасными, такие как заголовки браузера (например, такие вещи, как пользовательский агент, request.META['HTTP_USER_AGENT'] в Django), тривиально вмешиваются либо непосредственно в браузере или с помощью таких инструментов, как Burp или Чарльз.

Практически в Django это означает почти все, что не связано с Объект HttpRequest, то есть параметр request, который передается в качестве первого аргумента для функций просмотра. Хотя есть некоторые исключения, вероятно, лучше всего рассматривать что-либо в request как принципиально ненадежное.

Однако тот факт, что часть данных не прикреплена к request прямо сейчас, не означает, что вы можете доверять им. Например, рассмотрим что-то вроде подписи к изображению. Вы можете получить к нему доступ через API, в котором не упоминается запрос:

image = Image.objects.get(...)
sql = f"""SELECT * FROM images WHERE similarity(caption, '{image.caption}') > 0.5;
...

Но если эта подпись к изображению была ранее введена пользователем…это по-прежнему опасно. Это подводит нас ко второму правилу: всегда используйте параметризованные операторы.

Параметризованные операторы - это механизм передачи любых динамических параметров отдельно от SQL-запроса. Они либо интерпретируются непосредственно базой данных, либо безопасно экранируются перед добавлением в запрос. Почти каждый клиент базы данных на планете поддерживает параметризованные операторы - и если ваш не поддерживает, найдите другой.

Вот как будет выглядеть функция поиска, описанная выше, с параметризованными утверждениями:

def search(request):
    cursor = db.cursor()
    cursor.execute(
        "SELECT * FROM some_table WHERE title LIKE '%?%'",
        [request.GET['q']]
    )

Обратите внимание на ? в строке SQL и второй параметр execute. Этот второй аргумент — список параметров; элементы в этом списке безопасно вставляются в запрос для замены вопросительных знаков.

PEP-249, стандарт API базы данных Python требует параметризованных операторов, хотя разные библиотеки могут использовать разный синтаксис для заполнителей (параметры в стиле %, параметры :named, числовые параметры и т. д.).

Вы можете использовать инструменты анализа кода для проверки SQL-инъекций. Bento — один из таких инструментов, в котором есть несколько проверок распространенных проблем с SQL-инъекциями. Это может выявить множество распространенных ошибок; но по-прежнему рекомендуется использовать параметризованные операторы и один из приведенных ниже методов, чтобы полностью предотвратить эту атаку.

Предотвращение SQLi в Django

Django ORM повсеместно использует параметризованные операторы, поэтому он очень устойчив к SQLi. Таким образом, если вы используете ORM для выполнения запросов к базе данных, вы можете быть уверены, что ваше приложение в безопасности.

Но все же есть несколько случаев, когда необходимо помнить об инъекционных атаках; очень небольшое меньшинство API не является на 100% безопасным. Именно на них вы должны сосредоточить свой аудит, и именно на них должен быть направлен автоматизированный анализ кода.

Сырые запросы

Иногда ORM недостаточно выразителен, и вам нужен чистый SQL. Прежде чем сделать это, подумайте, есть ли способы избежать этого, например, построение модели Django поверх представления базы данных или вызов хранимой процедуры может помочь избежать необходимости встраивать необработанный SQL в ваш Python.

Но иногда необработанный SQL неизбежен. Для этого существует несколько API, но все они в некоторой степени опасны. В порядке желательности, вот API, которые предоставляет Django:

  1. Необработанный запросы, например:

     sql = "... some complex SQL query here ..."
     qs = MyModel.objects.raw(sql, [param1, param2])
     # ^ note the parameterized statements in the line above
  2. Аннотация RawSQL, например:

     from django.db.models.expressions import RawSQL
     
     sql = "... some complex subquery here ..."
     qs = MyModel.objects.annotate(val=RawSQL(sql, [param1]))
     # ^ note the parameterized statement in the line above
  3. Использовать курсоры базы данных напрямую, например:

     from django.db import connection
     sql = "... some complex query here ..."
     with connection.cursor() as cursor:
      cursor.execute(sql, [param1])
      # ^ again, note the parameterized statement in the line above
  4. ИЗБЕГАЙТЕ: Queryset.extra() (без примера: это небезопасно, поэтому оно включено только для полноты картины).

Для безопасного использования этих API:

  1. Прочитайте первую часть этой статьи и убедитесь, что понимаете параметризованные операторы, прежде чем продолжить.
  2. Не используйте extra(). Его сложно (если не невозможно) использовать на 100 % безопасно, и его следует считать устаревшим.
  3. Всегда передавать параметризованные операторы — даже если ваш список параметров пуст. То есть вы должны написать что-то вроде:

     sql = 'SELECT * FROM something;'
     qs = MyModel.objects.raw(sql, [])

Это сделано для того, чтобы напомнить вам о необходимости позже добавить параметры в этот список, а также для того, чтобы автоматизированным инструментам, таким как Bento, было проще найти потенциально неправильное использование API.

Самый запрос всегда должен быть статической строкой, а не сформированной в результате конкатенации или любой другой обработки строки. Опять же, это делается для того, чтобы автоматическим инструментам было легче найти неправильное использование API.

Автоматическая профилактика

Полезно использовать инструменты анализа кода для выявления предотвратимых ошибок - как говорится, ошибаться свойственно человеку. Bento автоматически проверяет код Django на наличие шаблонов SQL-инъекций. Ниже приведена проверка вашей кодовой базы на наличие SQL-инъекций, вызванных тем, что что-то висит на объекте запроса.

pip3 install bento-cli && \
  bento init && \
  BENTO_REGISTRY=r/r2c.python.django.security.injection.sql bento check -t semgrep --all .

Однако лучше, чем проверка текущего кода, - это проверка будущего кода! Bento разработан для запуска в качестве крючка перед коммитом или в средах непрерывной интеграции (CI). Bento ориентирован на работу с диффами и будет проверять только коммиты, обеспечивая скорость рабочего процесса и сохраняя безопасность вашего кода. Когда вы запустите Bento в своем проекте, он автоматически настроит себя на проверку коммитов.

Этот рабочий процесс, основанный на фиксации, особенно эффективен для обеспечения того, чтобы определенные шаблоны никогда не попадали в вашу кодовую базу. Чтобы практически исключить SQL-инъекции из вашей кодовой базы, вы должны автоматически определять, что ваш код:

  1. Always uses parameterized queries.
  2. Never uses .extra().

Bento может обнаружить эти шаблоны, используя другой реестр:

BENTO_REGISTRY=r/r2c.python.django.security.audit bento check -t semgrep --all .

Этот набор правил выявит гораздо больше результатов, даже если уязвимости нет. Это намного строже и может быть ошеломляющим, если вы проверяете свой код сразу. Однако вы также можете архивировать свои выводы с помощью Bento, который будет скрывать их до тех пор, пока вы не будете готовы с ними работать. Это позволяет вам постоянно проверять свой код на наличие этих шаблонов, не перегружаясь результатами.

Под капотом Bento работает на Semgrep. Semgrep - это инструмент для легкого обнаружения и предотвращения ошибок и антипаттернов в вашей кодовой базе. Он сочетает в себе удобство grep с корректностью синтаксического и семантического поиска. Это имеет преимущества перед обычным grep - наиболее очевидное из них заключается в том, что Semgrep не мешают границы строк.

Допустим, вы хотите обнаружить следующую SQL-инъекцию:

def search(request):
    search_term = request.GET['search_term']
    cursor = db.cursor()
    cursor.execute("SELECT \* FROM table WHERE field=" + search_term)

Это можно выразить в Semgrep следующим образом:

$VAR = request.GET[...]
...
$CUR.execute("..." + \$VAR)

Обнаружение такого шаблона в рабочем процессе на основе фиксации имеет неоценимое значение, поскольку он эффективно устраняет этот шаблон внедрения SQL из вашей кодовой базы! Вы можете проверить это в действии на странице https://sgrep.live/0X5.< /p>

Другие ORM

И наконец, если вы постоянно сталкиваетесь с тем, что ORM в Django недостаточно выразителен, возможно, вы захотите поэкспериментировать с заменой ORM в Django на SQLAlchemy, который является более мощным и выразительным ORM. Вы потеряете многие удобства Django, такие как администратор, модельные формы и общие представления на основе моделей, но получите более мощный и выразительный API, который по-прежнему безопасен.

Дополнения к ORM

Наконец, есть несколько потенциально опасных областей, которые могут быть небезопасными, даже если вы не используете чистый SQL напрямую. Django позволяет создавать пользовательские агрегаты и пользовательские выражения — например сторонняя библиотека может написать такие API, что что-то вроде Document.objects.filter(title__similar_to=other_title) будет работать.

Ядро ORM в Django - основные выражения, аннотации и агрегации - все они зрелые и закаленные в боях. Вероятность возникновения SQLi в основных частях ORM очень и очень мала. Но дополнения ORM - особенно те, которые вы пишете сами - все еще могут быть источником риска.

Чтобы снизить риск инъекций от этих расширенных функций, я предлагаю следующее:

Во-первых, будьте осторожны при включении пользовательских выражений/агрегатов из сторонних приложений. Вы должны тщательно проверить эти сторонние приложения. Является ли приложение зрелым, стабильным и поддерживаемым? Уверены ли вы, что любые проблемы безопасности будут быстро устранены и ответственно раскрыты? И, конечно, не забудьте зафиксировать свои зависимости, чтобы предотвратить установку более новых и потенциально менее безопасных версий без вашего явного указания.

Точно так же будьте осторожны при написании собственных настраиваемых агрегатов. Внимательно прочитайте начало этой статьи и Документация Django о том, как избежать внедрения кода SQL в пользовательские выражения. Как показывает документация, по возможности следует избегать какой-либо интерполяции строк в пользовательских выражениях. Если вы не можете, вам нужно самостоятельно экранировать любые параметры выражения. Это сложно сделать правильно, и это будет зависеть от специфики вашего ядра базы данных и API-интерфейса оболочки Python. Проконсультируйтесь со специалистом, прежде чем погрузиться в это!

Реестр django.security.audit в Bento обнаружит, если в вашей кодовой базе определено пользовательское дополнение ORM; с его помощью можно также быстро провести аудит приложений сторонних разработчиков. Условия эксплуатации очень тонкие, поэтому если вы обнаружите это в своем проекте, обязательно проконсультируйтесь с экспертом!

Подведение итогов

Django был разработан таким образом, чтобы быть устойчивым к SQL-инъекциям (и другим распространенным веб-уязвимостям). Большинство распространенных вариантов использования Django будут автоматически защищены, поэтому уязвимости SQLi в реальных приложениях Django, к счастью, встречаются редко.

Как бы то ни было, когда они возникают, уязвимости SQLi являются разрушительными. Стоит потратить время на аудит своей кодовой базы, чтобы убедиться, что вы в безопасности. Bento может помочь в этом, отмечая несколько распространенных уязвимостей. Теперь, когда вы понимаете концепцию и то, почему определенные ошибки отмечаются, вы должны быть лучше подготовлены к написанию безопасного кода.

Вернуться на верх