
Уязвимость SQL-инъекции в Django
Django — это веб-фреймворк с открытым исходным кодом, написанный на Python и построенный по модели MVC (Model-View-Controller). Изначально он был создан для управления новостными веб-сайтами, принадлежащими издательской корпорации Lawrence, как CMS (Content Management System).
Версии Django 3.1.x -> 3.1.13 и 3.2.x -> 3.2.5 содержат уязвимость SQL-инъекции.
Причина этой уязвимости в том, что функция фильтрации входных данных, контролируемых пользователем, в QuerySet.order_by() недостаточна для защиты от атак SQL-инъекций. Данная уязвимость может быть использована, позволяя злоумышленнику выполнять несанкционированные действия, что приводит к утечке конфиденциальных данных.
| CVE - ID | CVE-2021-35042 |
|---|---|
| Критичность | 9.8 - КРИТИЧЕСКИЙ |
| CWE - ID | CWE-89: Неправильная нейтрализация специальных элементов, используемых в SQL-команде ('SQL-инъекция') |
| Дата публикации уязвимости | 1/7/2021 |
| Затронутое ПО | 3.1.x < 3.1.13, 3.2.x < 3.2.5 |
| Требуется аутентификация | Не требуется |
В Django создание таблиц и определение полей в базе данных выполняется путём объявления класса модели в файле models.py. В этом примере мы объявляем таблицу с именем Wolf и поле с именем name.


ORM-фреймворк, встроенный в Django, используется для работы с базой данных, а результат запроса представляет собой набор, который называется QuerySet.
order_by(fields)
По умолчанию order_by() возвращает QuerySet, отсортированный в порядке, указанном в параметре ordering в Meta модели. Мы можем переопределить условие order_by в каждом запросе с помощью метода order_by().
Пример
wolves = Wolf.objects.order_by('-name', 'id')
Результат этого запроса будет отсортирован по полю name по убыванию, затем по id по возрастанию. Знак минуса перед полем name означает сортировку результатов по убыванию.
Следующий пример сортирует возвращаемые результаты по полю, полученному от пользователя; если значение не передано, сортировка выполняется по полю
id.
Результат

В версиях 3.1 и 3.2 Django позволяет комбинировать метод запроса с именем таблицы в запросе order_by. Это и есть основная причина данной уязвимости.
Передача имени таблицы даёт тот же результат, что и обычная передача имени поля
cve202135042_wolf — это имя таблицы
Сначала приложение напрямую вызывает функцию order_by(); код, обрабатывающий функцию order _by(), определён в:
django/db/models/query.py

Функция order_by() выполняет две задачи
- Удаляет все текущие условия, установленные order_by(), и удаляет параметр по умолчанию, переданный, когда order_by получает другое значение.
- Передаёт параметр в order_by. Это выполняет функция
add_ordering().
def add_ordering(self, *ordering):
"""
Add items from the 'ordering' sequence to the query's "order by"
clause. These items are either field names (not column names) --
possibly with a direction prefix ('-' or '?') -- or OrderBy
expressions.
If 'ordering' is empty, clear all ordering from the query.
"""
errors = []
for item in ordering:
if isinstance(item, str):
if '.' in item:
warnings.warn(
'Passing column raw column aliases to order_by() is '
'deprecated. Wrap %r in a RawSQL expression before '
'passing it to order_by().' % item,
category=RemovedInDjango40Warning,
stacklevel=3,
)
continue
if item == '?':
continue
if item.startswith('-'):
item = item[1:]
if item in self.annotations:
continue
if self.extra and item in self.extra:
continue
# names_to_path() validates the lookup. A descriptive
# FieldError will be raise if it's not.
self.names_to_path(item.split(LOOKUP_SEP), self.model._meta)
elif not hasattr(item, 'resolve_expression'):
errors.append(item)
if getattr(item, 'contains_aggregate', False):
raise FieldError(
'Using an aggregate in order_by() without also including '
'it in annotate() is not allowed: %s' % item
)
if errors:
raise FieldError('Invalid order_by arguments: %s' % errors)
if ordering:
self.order_by += ordering
else:
self.default_ordering = False
Параметр, передаваемый в add_ordering(), — это массив.
Пример передачи параметра:
wolves = Wolf.objects.order_by( 'name' , 'id' )В этом случае приложение преобразует его в следующий запрос к БД:
SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY "cve202135042_wolf"."name" ASC, "cve202135042_wolf"."id" ASC
При передаче функция add_ordering проверяет каждый элемент массива; если элемент является string, он проверяется в следующих пяти случаях:
if '.' in item:Проверяется, является ли это запросом с именем столбца, содержащим указанное в SQL-выражении имя таблицы. Если да — выводится предупреждение и выполняетсяcontinue.if item == '?':Если значением элемента является символ '?', выходные результаты сортируются случайным образом,continue.if item.startswith('-'):Если элемент начинается с символа '-', результаты запроса сортируются по убыванию (DESC).if item in self.annotations:Проверяется, содержит ли элемент аннотацию; если да,continue.if self.extra and item in self.extra:Определяется, есть ли дополнительное значение; если да,continue.
После пяти проверок параметр передаётся в функцию self.names_to_path(item.split(LOOKUP_SEP), self.model._meta) для дальнейшей проверки, является ли он допустимым именем столбца; если да, он добавляется в self.ordering класса Query для дальнейшей обработки.
ORM Django очень строго фильтрует данные, попадающие в запрос, но изменение исходного кода на этот раз привело к SQL-инъекции из-за предположения автора, что если имя столбца является UUID (Universal Unique Identifier), то запрос order_by не может быть выполнен.
Это означает, что если переданные данные имеют вид xxx-xxx-xxx-xxx (формат UUID), запрос не может быть выполнен.
Код до внесения изменений
# django/db/models/sql/constants.py
ORDER_PATTERN = _lazy_re_compile ( r '\?|[-+]?[.\w]+$' )