В сервисе заказов пользователь открывает свой заказ по адресу GET /orders/{order_id}. Пользователь A знает идентификатор заказа B и подставляет его в запрос. Если сервер возвращает детали заказа B, он не проверил право пользователя A читать эту запись.
IDOR возникает, когда по идентификатору из запроса можно получить объект без проверки прав на этот объект. Разберём различие между аутентификацией и авторизацией, воспроизведём ошибку в FastAPI и проверим исправление, которое сохраняет доступ к собственным заказам.
Сценарий с A и B учебный. Он не описывает проникновение в действующий сервис. Для проверки использованы только локальные синтетические данные, два заранее известных пользователя и два заказа.
Аутентификация и разрешение на конкретный заказ
Успешная аутентификация не подтверждает право пользователя читать любой заказ. Сервер должен понимать не только, кто отправил запрос, но и какое действие ему разрешено над конкретной записью.
OWASP API Security Top 10 (2023) называет нарушение проверки доступа к объекту Broken Object Level Authorization (BOLA). В этом сценарии пользователь уже имеет право вызывать GET /orders/{order_id}; нарушение возникает при выборе чужой записи. Описание OWASP API1:2023 отделяет этот случай от доступа к полностью запрещённой функции API.
На сервере нужно сопоставлять запрошенный объект с правами текущего пользователя при каждом обращении. Такое правило важно и для чтения, и для операций изменения данных: оно не зависит от того, как клиент построил URL.
В тестовой модели разрешение зависит от совпадения пользователя, установленного сервером, и владельца заказа. Все запросы проходят через один маршрут; разрешение читать конкретную запись зависит от пользователя.
| Запрос | Текущий пользователь | Владелец заказа | Требуемый результат |
|---|---|---|---|
GET /orders/101 | A | A | 200, собственные данные |
GET /orders/202 | A | B | отказ без чужих данных |
GET /orders/202 | B | B | 200, собственные данные |
GET /orders/999 | A | записи нет | 404 |
Значение owner_id, присланное клиентом, не подтверждает разрешение: пользователь может менять его так же, как идентификатор заказа. Проверка должна использовать идентичность из проверенного серверного контекста.
Где ошибается уязвимый обработчик
В учебном обработчике FastAPI заказ ищется по order_id. В уязвимом режиме запись возвращается любому аутентифицированному пользователю: проверка владельца отсутствует.
Демонстрационный токен подтверждает личность, но идентификатор из URL не подтверждает право читать выбранный заказ. Руководство OWASP по IDOR рассматривает не только последовательные числа, но и UUID, имена файлов и другие ссылки на объекты.
Замена числового идентификатора на UUID не заменяет проверку доступа. Сложные идентификаторы затрудняют угадывание, но не препятствуют использованию известной ссылки на чужой объект.
Минимальное приложение для двух вариантов
Для локальной демонстрации использовались Python 3.12.12, FastAPI 0.128.2, Starlette 0.50.0 и HTTPX 0.28.1. Логика файла idor_app.py приведена ниже. Он хранит два заказа в памяти и принимает только вымышленные демонстрационные токены. Словарь токенов не является безопасной реализацией аутентификации для рабочего сервиса.
"""Two in-memory order APIs for an isolated object-authorization exercise."""
from typing import Annotated, TypedDict
from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.security import OAuth2PasswordBearer
class Order(TypedDict):
id: int
owner_id: str
item: str
# Synthetic identities and orders: this is not a production authentication scheme.
TOKEN_USERS = {"demo-a": "A", "demo-b": "B"}
ORDERS: dict[int, Order] = {
101: {"id": 101, "owner_id": "A", "item": "Notebook"},
202: {"id": 202, "owner_id": "B", "item": "Headphones"},
}
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> str:
user = TOKEN_USERS.get(token)
if user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid demo token")
return user
def make_app(*, vulnerable: bool = False) -> FastAPI:
"""Create a fixed API by default; expose the vulnerable path only to TestClient."""
app = FastAPI()
@app.get("/orders/{order_id}")
def get_order(
order_id: int,
request: Request,
user: Annotated[str, Depends(current_user)],
) -> Order:
if vulnerable and (request.client is None or request.client.host != "testclient"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Vulnerable demo is in-process only",
)
order = ORDERS.get(order_id)
if order is None or (not vulnerable and order["owner_id"] != user):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Order not found")
return order
return app
make_app(vulnerable=True) намеренно отключает проверку владельца и применяется только как отрицательный контроль локального теста. При запросе не от встроенного TestClient этот режим возвращает 403 до чтения заказа; его всё равно нельзя использовать как рабочий сервис. Обычный вызов make_app() создаёт исправленный обработчик.
Проверка current_user присутствует в обеих версиях. Это важно для диагностики: ошибка возникает после аутентификации, когда запись выбирается через ORDERS.get(order_id) без проверки владельца.
Как отличить нарушение доступа от обычного ответа API
Сначала пользователь A читает свой заказ 101. Затем с тем же токеном обращается к заказу 202, который принадлежит B. Обе версии обработчика получают одинаковые исходные данные.
OWASP WSTG рекомендует искать точки прямого обращения к объектам и проверять замену идентификатора. Методика WSTG-ATHZ-04 требует оценивать результат в контексте доступных пользователю данных.
В локальном тесте нарушение подтверждено: пользователь A получил заказ B с кодом 200 и идентификатором 202. В исправленной версии тот же пользователь получил 404 без содержимого чужого заказа. Ниже приведён фактический вывод двух версий.
Фактический вывод
Исходники и закреплённые зависимости доступны в учебном проекте XNova на точном проверенном commit. После установки по README из корня проекта запустите:
.venv/bin/python -m examples.check_idor
На Windows используйте .venv\Scripts\python.exe вместо .venv/bin/python. Оба приложения запускались внутри FastAPI TestClient, без открытого сетевого порта. Вывод сохранён без изменений:
vulnerable_a_own: status=200 order_id=101
vulnerable_a_foreign: status=200 order_id=202
fixed_a_own: status=200 order_id=101
fixed_a_foreign: status=404 order_id=None
fixed_b_own: status=200 order_id=202
fixed_missing_order: status=404 order_id=None
fixed_anonymous: status=401 order_id=None
fixed_invalid_token: status=401 order_id=None
Во второй строке уязвимый обработчик отдаёт заказ B пользователю A. В четвёртой исправленный обработчик отказывает при том же токене и идентификаторе. Свой заказ A читает успешно в обеих версиях.
Граница наблюдения: тест проверяет конкретные объекты в памяти и фиксированный набор запросов, а не внешнюю API-систему.
Проверка доступа при выборке заказа
В учебной модели заказ принадлежит одному пользователю. Поэтому обработчик берёт текущего пользователя из серверной зависимости и проверяет owner_id найденной записи до отправки ответа.
Документация FastAPI показывает передачу текущего пользователя в обработчик через зависимость. В реальном сервисе права могут зависеть от роли, организации и совместного доступа: простой owner_id пригоден только там, где именно владение определяет разрешение.
Проверка владельца перед возвратом заказа
В защищённом варианте проверка расположена между выборкой и возвратом заказа:
order = ORDERS.get(order_id)
if order is None:
raise HTTPException(status_code=404, detail="Order not found")
if order["owner_id"] != user:
raise HTTPException(status_code=404, detail="Order not found")
return order
Переменная user получена из current_user; обработчик не принимает значение владельца из запроса. В рабочем сервисе зависимость должна проверять настоящую сессию или токен.
Одинаковый 404 для чужой и отсутствующей записи выбран, чтобы не подтверждать существование чужого заказа. В другом приложении может быть обоснован 403; важно, чтобы данные не возвращались. 401 остаётся ответом на отсутствие или недействительность аутентификации.
В базе данных доступ удобно ограничивать непосредственно в запросе, одновременно проверяя order_id и владельца из серверного контекста. При совместном доступе, организациях и ролях потребуется более полная доменная политика разрешений.
Тест, который защищает границу между пользователями
FastAPI позволяет проверять обработчики через TestClient без запуска внешнего HTTP-сервера. Руководство FastAPI показывает такой тест с HTTPX и pytest. Здесь каждый тест получает новое приложение через фикстуру pytest, поэтому состояние одного сценария не переходит в следующий.
В тестах FastAPI можно подменять зависимости через app.dependency_overrides. Руководство по подмене зависимостей показывает, как заменить текущего пользователя в тесте. После проверки подмена очищается: следующий запрос снова использует исходную зависимость.
Помимо разрешённых запросов нужно проверить чужой и отсутствующий заказы, отсутствие токена и неверный токен. Уязвимый вариант запускается отдельно как отрицательный контроль, чтобы доказать существование исходного дефекта.
Десять проверок и отрицательный контроль
Десять тестов охватывают эти случаи, подмену зависимости и оба режима при другом клиентском адресе. Две ключевые проверки из tests/test_idor.py:
def test_other_user_cannot_read_order(client: TestClient) -> None:
response = get_order(client, 202)
assert response.status_code == 404
assert "Headphones" not in response.text
def test_vulnerable_variant_exposes_foreign_order() -> None:
with TestClient(make_app(vulnerable=True)) as client:
response = get_order(client, 202)
assert response.status_code == 200
assert response.json()["owner_id"] == "B"
После установки зависимостей из корня учебного проекта выполните:
.venv/bin/python -m pytest -q
На Windows используйте .venv\Scripts\python.exe. Pytest сообщает 10 passed; время выполнения зависит от машины. Десять тестов завершились успешно. Код выхода наблюдательного сценария и тестов — 0.
Если проверка чужого заказа неожиданно возвращает 200, проверьте условие владельца и убедитесь, что запущен исправленный вариант. Если подмена пользователя влияет на следующий запрос, проверьте очистку app.dependency_overrides в test_dependency_override_is_cleared.
Где ещё проверять доступ к объектам
Такой же дефект возможен при изменении и удалении записи, скачивании вложения или массовой операции. OWASP Authorization Cheat Sheet рекомендует проверять разрешения при каждом обращении к защищённому ресурсу.
| Операция | Проверка |
|---|---|
GET и скачивание файлов | Право на вложение должно соответствовать праву на связанный заказ |
PATCH и DELETE | Запрет чтения чужого объекта не гарантирует запрет его изменения или удаления |
| Списки и массовые операции | Выборка и обработка каждой записи должны соблюдать область прав пользователя |
| Совместный доступ | Нужно учитывать роли, организацию и отзыв разрешения |
Идентификатор может находиться в URL, параметрах запроса или JSON-теле. Сложный UUID затрудняет подбор чужой ссылки, но не заменяет проверку права доступа.
Учебный пример ограничен двумя пользователями, двумя заказами и личным владением. Реальная база данных, провайдер токенов, несколько организаций, делегирование прав и параллельные запросы требуют самостоятельных проверок.
Что необходимо закрыть при исправлении IDOR
Проверяйте право пользователя на конкретный заказ до возврата данных. Действующая сессия и сложный идентификатор сами по себе такого права не дают.
В локальной модели это поведение проверено: до исправления A получил чужой заказ с 200, после исправления — 404, собственный заказ остался доступен. Десять тестов pytest успешно прошли на объявленных версиях Python/FastAPI/HTTPX.
В рабочем API установите происхождение текущего пользователя, определите права на каждый объект и добавьте отрицательные тесты для чужих данных. Сценарии с реальными токенами, организациями и делегированными правами выходят за пределы показанной модели.
Источники
- Учебный FastAPI-проект XNova: исходники, закреплённые зависимости и команды воспроизведения на проверенном commit
- OWASP API1:2023 BOLA: граница объектной авторизации, типовые сценарии и профилактика
- OWASP IDOR Prevention: идентификаторы, недостаточность UUID и проверка доступа
- OWASP WSTG: как тестировать доступ к объектам при замене пользовательских идентификаторов
- OWASP Authorization Cheat Sheet: проверка прав на конкретный ресурс при каждом запросе
- Официальный FastAPI TestClient с HTTPX и pytest
- Официальный FastAPI: подмена зависимостей в тестах и очистка overrides
- Официальный FastAPI: извлечение текущего пользователя через dependency injection