Безопасность API (OWASP API Top 10)

В мире API дыра номер один — не «взлом шифрования», а проверка прав, которую забыли сделать на сервере.

API (Application Programming Interface) — интерфейс, через который приложения обмениваются данными (обычно REST/JSON или GraphQL). OWASP API Security Top 10 — список самых частых классов уязвимостей именно у API, отдельный от классического веб-Top-10.

Мобильные приложения, SPA и интеграции общаются с бэкендом через API, и атакующий видит ровно те же запросы, что и легальный клиент: их легко перехватить и повторить с изменёнными параметрами. Защитнику важно понять три самых распространённых и болезненных класса из OWASP API Top 10 — нарушение авторизации на уровне объекта, чрезмерную выдачу данных и отсутствие лимитов — и научиться закрывать их на сервере. Главный принцип: клиенту доверять нельзя, всё проверяется на бэкенде.

Зачем это знать защитнику

API-уязвимости массовы, потому что фронтенд «выглядит правильно», а проверки на сервере нет. Понимая логику этих ошибок, вы добавляете недостающие проверки до того, как их найдёт кто-то снаружи. Проверять можно только свои API и легальные тренировочные стенды (juice-shop, DVWA, TryHackMe) либо пентест с письменным согласием владельца. Дёргать чужой прод — несанкционированный доступ (ст. 272 УК РФ).

API1: Broken Object Level Authorization (BOLA / IDOR)

Самый частый и самый критичный класс. Эндпоинт принимает идентификатор объекта (/api/orders/1024) и отдаёт его, проверив, что пользователь залогинен, но забыв проверить, что объект принадлежит именно ему. Атакующий просто меняет 1024 на соседний номер и читает чужие заказы.

Как это возникает в коде

# УЯЗВИМО: проверена аутентификация, но не владение объектом
@app.get("/api/orders/{order_id}")
def get_order(order_id: int, user = Depends(current_user)):
    return db.orders.get(order_id)        # вернёт ЛЮБОЙ заказ по id

Как защититься

Проверка владения должна быть в самом запросе к данным — фильтруйте по владельцу, а не «достал и отдал»:

# БЕЗОПАСНО: объект ищется в рамках владельца
@app.get("/api/orders/{order_id}")
def get_order(order_id: int, user = Depends(current_user)):
    order = db.orders.get(order_id, owner_id=user.id)   # привязка к владельцу
    if order is None:
        raise HTTPException(404)          # не существует ИЛИ не ваш — одинаково
    return order

Дополнительно: используйте непредсказуемые идентификаторы (UUID вместо последовательных чисел) — это не замена проверке прав, но усложняет перебор. И отдавайте 404 вместо 403, чтобы не подтверждать существование чужих объектов.

API3: Excessive Data Exposure (чрезмерная выдача)

Бэкенд возвращает весь объект целиком — «фронтенд сам покажет только нужное». Но фильтрация на клиенте не защищает данные: в ответе по сети едут passwordHash, внутренние флаги, e-mail и телефоны других пользователей. Атакующий смотрит сырой JSON, а не отрисованную страницу.

Как защититься

Сервер отдаёт только те поля, что нужны клиенту, через явные схемы ответа (DTO/serializer), а не «модель как есть»:

# Явная схема ответа: лишние поля просто не попадут наружу
class UserPublic(BaseModel):
    id: int
    name: str
    # password_hash, email, is_admin — НЕ включены

@app.get("/api/users/{uid}", response_model=UserPublic)
def get_user(uid: int):
    return db.users.get(uid)     # вернутся только id и name

Правило: allow-list полей (явно перечислить, что отдаём), а не deny-list (пытаться вычеркнуть лишнее — однажды забудете). То же касается массовых выдач (/api/users): отдавайте публичный минимум.

API4: Unrestricted Resource Consumption (нет лимитов)

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

Как защититься

МераЧто закрывает
rate limiting (лимит запросов в минуту)перебор, DoS
пагинация с потолком limitвыкачивание всей базы за раз
лимит размера тела запросаперегрузку памяти большими payload
блокировка/задержка после N неудачbrute-force логина и OTP
# Идея ответа сервера при превышении лимита
HTTP/1.1 429 Too Many Requests
Retry-After: 60

Как это работает под капотом

API без состояния (stateless): каждый запрос несёт токен, и сервер на каждый запрос обязан заново ответить на два вопроса — «кто это?» (аутентификация) и «можно ли ему именно это с именно этим объектом?» (авторизация). BOLA — это пропущенная проверка второго вопроса для конкретного объекта. Excessive Data Exposure — лишние поля в ответе, потому что сериализуется вся модель. Отсутствие лимитов — нет учёта частоты и объёма запросов. Сюда же примыкает ещё один важный класс из списка — Broken Function Level Authorization (BFLA): когда обычному пользователю доступны административные действия (например, DELETE /api/users/{id}), потому что права на уровне функции/метода не проверяются. Лечится это так же — явной проверкой роли и прав на сервере для каждого действия, а не сокрытием кнопки в интерфейсе. Всё это — серверные пробелы, и закрываются они только на сервере: клиент в защите не участвует.

Как защититься

  • Проверяйте владение объектом на каждом эндпоинте: ищите данные в рамках текущего пользователя, отдавайте 404 для чужого.
  • Отдавайте только нужные поля через явные схемы ответа (allow-list), никогда не «модель целиком».
  • Ставьте rate limiting, пагинацию с потолком и лимиты размера запроса; усиливайте защиту на логине/OTP.
  • Валидируйте вход на сервере, версионируйте API, ведите аудит-логи и алёрты на аномалии (всплеск 404/401, перебор id).
  • Регулярно прогоняйте свой API сканером (например, OWASP ZAP) на своём стенде, а не на чужом проде.

Итоги

  • OWASP API Top 10 — отдельный список рисков именно для API; лидер — BOLA (проверка прав на объект).
  • Три ключевых класса: BOLA/IDOR, чрезмерная выдача данных, отсутствие лимитов.
  • Общий корень — доверие к клиенту; лекарство — все проверки на сервере.
  • Защита: владение объектом на каждом запросе, allow-list полей в ответе, rate limiting и пагинация.
  • Тестируйте только свои API и легальные стенды; чужой прод — это ст. 272 УК РФ.
Проверьте себя
1. В чём суть уязвимости BOLA (Broken Object Level Authorization, она же IDOR)?
AСервер шифрует данные слишком слабым алгоритмом
BПользователь аутентифицирован, но сервер не проверяет, что запрошенный объект принадлежит именно ему
CAPI возвращает ответ в формате XML вместо JSON
DТокен доступа передаётся в URL, а не в заголовке
2. Как правильно бороться с чрезмерной выдачей данных (Excessive Data Exposure)?
AФильтровать лишние поля на фронтенде перед показом пользователю
BОтдавать модель целиком, но сжимать ответ gzip
CИспользовать allow-list полей в явной схеме ответа на сервере
DШифровать весь JSON-ответ симметричным ключом