Перейти к содержимому

Статический анализ Idor в python: создаём ядро taint-анализа за 90 символов

Можно ли находить IDOR статическим анализом: создаём ядро для Python

IDOR - одна из тех уязвимостей, которые часто обнаруживаются не из-за сложной логики атаки, а из-за элементарного пропуска в проверке прав. Сервер принимает идентификатор объекта из URL или параметров запроса, находит запись в базе и возвращает её пользователю, не выясняя, имеет ли тот право на доступ.

На первый взгляд кажется, что такие ошибки должен легко находить статический анализатор. Однако обычного поиска по шаблонам недостаточно: один и тот же вызов ORM может быть как безопасным, так и уязвимым. Всё зависит от происхождения идентификатора, контекста запроса и того, учитывается ли владелец объекта.

Что такое IDOR

Insecure Direct Object Reference - это нарушение контроля доступа, при котором пользователь может обратиться к чужому объекту, изменив его идентификатор.

Представим приложение с личными счетами. Пользователь открывает адрес:

```text
/invoice/1001
```

Если заменить номер на `1002` и получить чужой документ, приложение содержит IDOR. Проблема не в том, что идентификаторы последовательные, и не в самом факте передачи ID через URL. Ошибка заключается в отсутствии серверной проверки: принадлежит ли запрошенный объект текущему пользователю.

Минимальный пример на Flask выглядит так:

```python
@app.route("/invoice/")
@login_required
def get_invoice(invoice_id):
invoice = Invoice.query.get(invoice_id)
return jsonify(invoice.to_dict())
```

Декоратор маршрута связывает URL с функцией, а `` извлекает числовой параметр. `login_required` удостоверяется только в том, что пользователь вошёл в систему. Затем приложение ищет запись по первичному ключу и отправляет её клиенту.

Аутентификация здесь есть, авторизации - нет. Любой зарегистрированный пользователь способен перебрать значения `invoice_id` и получить счета, которые ему не принадлежат.

Безопасный вариант должен ограничивать запрос владельцем:

```python
@app.route("/invoice/")
@login_required
def get_invoice(invoice_id):
invoice = Invoice.query.filter_by(
id=invoice_id,
owner_id=current_user.id
).first_or_404()

return jsonify(invoice.to_dict())
```

Другой подход - сначала получить объект, а затем сравнить его владельца с текущим пользователем. В обоих случаях принцип одинаков: сервер должен проверять право доступа к конкретному ресурсу.

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

Почему IDOR трудно искать статически

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

Кроме того, опасный идентификатор может пройти через несколько функций:

1. значение приходит из URL;
2. передаётся в обработчик;
3. преобразуется или сохраняется в переменной;
4. попадает в ORM-запрос;
5. результат сериализуется и возвращается клиенту.

Если анализатор смотрит только на строку `Model.query.get(id)`, он получит много ложных срабатываний. Такой запрос может использоваться во внутреннем административном разделе, где проверка выполняется в middleware, или получать идентификатор из доверенного источника.

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

Ограничения поиска по шаблонам

Простейшие инструменты обычно ищут комбинацию признаков:

- маршрут содержит динамический параметр;
- вызывается ORM-метод `get`, `filter_by` или аналогичный;
- результат возвращается пользователю;
- отсутствует очевидная проверка владельца.

Такой метод полезен как быстрый фильтр, но не как полноценное обнаружение IDOR. Он не понимает движение данных и не различает источник идентификатора. Для качественного анализа необходимо установить связь между входным значением и опасным использованием - то есть применить модель taint analysis.

Как устроить ядро taint-анализа

В taint-модели данные получают метку загрязнённых, если происходят из потенциально управляемого пользователем источника. К таким источникам относятся:

- параметры маршрутов Flask и FastAPI;
- значения query string;
- параметры POST-запросов;
- JSON-тело;
- cookies и заголовки;
- значения, извлечённые из форм.

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

Финальная точка - чувствительное использование данных. Для IDOR это обычно:

- поиск объекта по идентификатору;
- обращение к записи через ключ;
- изменение или удаление ресурса;
- возврат объекта в HTTP-ответе.

Само по себе попадание tainted-значения в `get()` ещё не доказывает уязвимость. Поэтому ядро должно дополнительно анализировать наличие признаков авторизации.

Как принимается решение

Удобно разделить проверку на несколько этапов.

Сначала анализатор строит модель маршрута и определяет, какие параметры поступают от клиента. Затем он отслеживает эти параметры до ORM-вызова. После этого проверяет, используется ли в запросе ограничение по текущему пользователю.

Условно поток может выглядеть так:

```text
/invoice/

invoice_id

Invoice.query.get(invoice_id)

jsonify(invoice.to_dict())
```

Если между входом и выдачей нет условия вроде:

```python
owner_id=current_user.id
```

или явного сравнения владельца, анализатор формирует предупреждение.

При этом важно учитывать и положительные признаки защиты:

- фильтрацию по `current_user.id`;
- сравнение `object.owner_id` с идентификатором сессии;
- вызов проверенной функции авторизации;
- декоратор, который гарантированно выполняет проверку;
- использование политики доступа или permission-класса.

Такая архитектура позволяет не считать уязвимым каждый маршрут, где встречается числовой ID.

Почему это не обычный поиск строк

Главное отличие от поиска по регулярным выражениям состоит в анализе потока данных. Движок должен понимать не только наличие отдельных конструкций, но и их взаимосвязь.

Например, эти варианты могут быть эквивалентны с точки зрения безопасности:

```python
invoice = Invoice.query.filter_by(
id=invoice_id,
owner_id=current_user.id
).first()
```

и:

```python
invoice = Invoice.query.get(invoice_id)

if invoice.owner_id != current_user.id:
abort(403)
```

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

Также необходимо учитывать алиасы:

```python
requested_id = invoice_id
invoice = Invoice.query.get(requested_id)
```

или передачу данных в отдельную функцию:

```python
def load_invoice(identifier):
return Invoice.query.get(identifier)
```

Чем больше таких случаев поддерживает движок, тем меньше он зависит от конкретного стиля программирования.

Ложные срабатывания и пропуски

Полностью надёжный статический детектор IDOR практически недостижим. В реальном коде права могут проверяться:

- в middleware;
- в базовом классе контроллера;
- через декоратор;
- внутри репозитория;
- в сервисе, вызываемом из обработчика;
- политикой, реализованной вне анализируемого файла.

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

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

Практическая ценность на проектах

На небольших Python-проектах подобный анализ особенно полезен: архитектура там часто неоднородна, а проверки прав добавляются вручную в каждом обработчике. Статический инструмент быстро выявляет маршруты, где разработчик проверил вход в систему, но забыл про принадлежность объекта.

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

Хороший результат даёт комбинация:

1. статический анализ ищет подозрительные потоки данных;
2. ручная проверка подтверждает бизнес-логику;
3. динамические тесты проверяют доступ от разных пользователей;
4. регрессионные тесты не дают уязвимости вернуться.

Как улучшить точность

Для практического применения ядру понадобятся уровни уверенности. Например:

- высокий риск - параметр маршрута напрямую используется в запросе, а проверка владельца отсутствует;
- средний риск - объект загружается по пользовательскому ID, но права могут проверяться во внешнем вызове;
- низкий риск - подозрительный поток найден, однако ресурс явно помечен как публичный или административный.

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

Отдельно стоит анализировать операции изменения и удаления. IDOR в `GET` раскрывает данные, но аналогичная ошибка в `PUT`, `PATCH` или `DELETE` может позволить изменить чужой профиль, отменить заказ или удалить документ.

Так можно ли найти IDOR статическим анализом

Да, но с важной оговоркой: статический анализ способен находить не сам факт успешной атаки, а подозрительные цепочки, в которых внешний идентификатор достигает обращения к объекту без видимой проверки прав.

Надёжность подхода зависит от качества taint-модели, понимания фреймворка и настройки проекта. Поиск по шаблонам слишком примитивен, а полноценный анализ должен учитывать источники данных, их перемещение, опасные операции и признаки авторизации.

Наиболее реалистичная цель - не заменить DAST и ручное тестирование, а дополнить их. Статический анализ работает на этапе разработки, показывает проблемные места ещё до развёртывания и помогает проверять весь код, включая маршруты, которые динамический краулер не смог обнаружить.

В результате ядро для Python может стать не только детектором IDOR, но и базой для более общего taint-анализатора. Добавив новые источники, приёмники и правила очистки данных, на той же архитектуре можно искать SQL-инъекции, командные инъекции, небезопасные файловые операции и другие классы ошибок, где важен путь пользовательского ввода через программу.

Прокрутить вверх