IDOR и нарушение контроля доступа
Разбираем, как приложение случайно отдаёт чужие данные по id, и закрываем это серверной проверкой прав — на учебном стенде.
IDOR (Insecure Direct Object Reference) — уязвимость, при которой сервер отдаёт объект по идентификатору из запроса, не проверив, что текущий пользователь имеет право на этот объект.
IDOR — частный, очень распространённый случай категории Broken Access Control, которая возглавляет OWASP Top 10. Практикуемся на стендах с ролями и несколькими аккаунтами (OWASP Juice Shop, лабораторные ВМ). Просматривать чужие заказы или данные на реальном сервисе, перебирая id, — это несанкционированный доступ (ст. 272 УК РФ), даже если «просто из любопытства» и «ничего не менял».
Зачем это знать защитнику
IDOR коварен тем, что приложение работает правильно с точки зрения функциональности — оно просто забывает спросить «а можно ли этому пользователю?». Такую дыру не видно в обычном тестировании по сценарию: нужен взгляд защитника, который проверяет каждый доступ к объекту. Разработчик, понимающий IDOR, ставит проверку прав там, где её легко пропустить.
Как возникает уязвимость
Типичный эндпоинт принимает id объекта и достаёт его из базы — но проверяет только аутентификацию (пользователь вошёл), забывая про авторизацию (этот объект принадлежит ему):
# УЯЗВИМО: достаём по id, не проверив владельца
@app.get("/api/orders/<int:order_id>")
@login_required
def get_order(order_id):
order = Order.query.get(order_id) # любой order_id вернёт любой заказ
return order.to_json()
Пользователь видит в адресе свой заказ /api/orders/1043. Если поменять номер на 1042, сервер послушно вернёт чужой заказ — потому что в коде нет ни одной строки, сверяющей владельца. Аутентификация пройдена (человек залогинен), но авторизация отсутствует.
Почему это происходит так часто
Идентификаторы предсказуемы: последовательные числовые id сами подсказывают «соседние» объекты. Но дело не в самих числах — заменить id на случайный UUID помогает мало (это лишь усложняет угадывание, не закрывает дыру). Настоящая причина — проверка прав вынесена из логики или забыта. Любой ссылающийся на объект параметр (id в пути, в теле, в скрытом поле формы) контролируется клиентом и не может служить доказательством прав.
Как это находят на CTF
На стенде заводят два аккаунта. Под аккаунтом A открывают свой ресурс, фиксируют его id, затем под тем же аккаунтом подставляют id ресурса аккаунта B и смотрят отклик. Вернулись чужие данные (а не 403 Forbidden) — IDOR подтверждён. Перебор соседних id автоматизируют, но смысл проверки — найти эндпоинт без авторизации, чтобы добавить её.
Как это работает под капотом
Сервер доверяет идентификатору из запроса как «адресу» нужной строки и выполняет выборку WHERE id = ?. Сама по себе выборка корректна — ошибка в том, что между «нашли объект» и «отдали объект» нет шага «а имеет ли право спрашивающий». Контроль доступа — это серверное решение: клиент по определению не заслуживает доверия, ведь он может прислать любой id. Поэтому решение всегда принимается на сервере, на основе личности из проверенной сессии, а не из параметров запроса.
Как защититься
1. Привязывайте выборку к владельцу. Самый надёжный приём — запрашивать объект сразу в связке с текущим пользователем, чтобы чужой объект просто не находился:
# БЕЗОПАСНО: выбираем только среди заказов текущего пользователя
@app.get("/api/orders/<int:order_id>")
@login_required
def get_order(order_id):
order = Order.query.filter_by(
id=order_id, user_id=current_user.id
).first()
if order is None:
abort(404) # чужой или несуществующий — неотличимо
return order.to_json()
Чужой id теперь не пройдёт фильтр user_id=current_user.id и вернёт 404 — данные не утекают.
2. Явная проверка прав, если связка по владельцу не подходит. Для ролей и совместного доступа проверяйте право отдельно, до выдачи объекта:
# БЕЗОПАСНО: централизованная проверка доступа
order = Order.query.get_or_404(order_id)
if not can_access(current_user, order): # роль, владелец, шаринг
abort(403)
return order.to_json()
3. Проверяйте на сервере и для каждого действия. Скрытые поля, выпадающие списки и «неизменяемые» id на стороне клиента ничего не гарантируют — их легко подменить. Авторизуйте и чтение, и запись, и удаление. Не полагайтесь на то, что «ссылки на чужие объекты не видно в интерфейсе»: безопасность через сокрытие (security by obscurity) не работает.
4. Принцип «запрещено по умолчанию» и обнаружение. Доступ закрыт, пока явно не разрешён. Логируйте отказы (всплеск 403/404 по разным id с одного аккаунта — признак перебора) и тестируйте контроль доступа: автотест, который под одним пользователем дёргает объекты другого и ждёт 403/404, ловит регрессии раньше злоумышленника.
Где должна жить проверка доступа
Частая причина IDOR — проверка прав «расползается» по контроллерам, и в каком-то новом эндпоинте её просто забывают добавить. Надёжнее централизовать решение: единая функция или слой авторизации (политики, декораторы, middleware), через которые проходит каждый доступ к объекту. Тогда новый разработчик не сможет случайно отдать объект без проверки — она встроена в общий путь. Особенно внимательно относитесь к «массовым» эндпоинтам и фильтрам: запрос вида «верни все заказы» тоже должен ограничиваться текущим пользователем на уровне выборки из базы, иначе утечёт весь список разом.
Проверка в лаборатории
На стенде заведите два аккаунта и пройдите по списку эндпоинтов: под пользователем A попробуйте прочитать, изменить и удалить объекты пользователя B по их id. Ожидаемый результат для всех трёх действий — 403 или 404, а не чужие данные и не успешное изменение. Не забудьте про операции записи: бывает, что чтение защищено, а PUT/DELETE по тому же id — нет. Зафиксируйте этот сценарий как регрессионный тест: контроль доступа легко сломать рефакторингом, и автоматическая проверка — лучший способ это заметить.
Итоги
- IDOR — это нарушение контроля доступа: сервер отдаёт объект по id, не проверив права спрашивающего.
- Корень — путаница аутентификации (вошёл) и авторизации (имеет право); проверка прав забыта.
- Защита — привязка выборки к владельцу либо явная серверная проверка прав перед выдачей.
- Случайные UUID и сокрытие ссылок не заменяют авторизацию; проверяйте на сервере каждое действие.
- Перебор чужих id на реальном сервисе — несанкционированный доступ (ст. 272 УК РФ). Только стенд.