Защита API: лимиты, валидация и API Top 10

API — это дверь без интерфейса: атакующему даже не нужен браузер.

Rate limiting — ограничение числа запросов за интервал. OWASP API Top 10 — список самых частых рисков именно для API.

Чем API-безопасность отличается

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

Есть и психологическая ловушка. Когда веб-страница рендерит только те кнопки, на которые у пользователя есть права, разработчику легко поверить, что доступа к остальному нет. Но API — это голый контракт: атакующему не нужен ваш интерфейс, он открывает инструмент для отправки запросов и обращается к эндпоинтам напрямую, подставляя любые идентификаторы и любые поля. Всё, что не проверено на сервере, считается доступным. Отсюда главное смещение мышления: при работе с API нельзя полагаться ни на что, что происходит на клиенте, — ни на скрытые поля, ни на отключённые кнопки, ни на «незадокументированные» адреса.

Стоит понимать и масштаб последствий. Ошибка авторизации в обычной форме затрагивает одну страницу, а та же ошибка в API, который перебирают скриптом, мгновенно масштабируется: за минуту можно вытащить тысячи чужих записей, увеличивая идентификатор на единицу. Поэтому риски, которые в монолитном приложении казались мелкими, в API-мире выходят на первое место — и именно поэтому OWASP поддерживает отдельный список API Top 10, а не ограничивается общим перечнем веб-рисков.

Rate limiting

Без ограничения частоты API открыт для перебора паролей, скрейпинга данных и отказа в обслуживании. Лимиты ставят на чувствительные эндпоинты (вход, сброс пароля) и в целом на клиента/токен/IP.

// Концептуально: окно лимита по ключу клиента
key = clientId or ip;
count = store.incr(key, window=60);   // запросов за минуту
if (count > LIMIT) return response(429, "Too Many Requests");

Дополняйте лимитами по «дорогим» операциям и квотами, чтобы один клиент не исчерпал ресурсы для всех.

Важно понимать, что rate limiting решает сразу несколько разных задач, и это не только про DoS. Лимит на эндпоинт входа замедляет перебор паролей: даже зная логин, атакующий не сможет проверить миллион вариантов, если ему разрешено пять попыток в минуту. Лимит на сброс пароля и на отправку кодов мешает заваливать пользователей письмами и SMS (а заодно бережёт ваш бюджет на рассылку). Лимит на эндпоинты выдачи списков мешает массовому скрейпингу — выкачиванию всего каталога или всех профилей. И, наконец, общий лимит на клиента защищает сервис от случайной или намеренной перегрузки. Поэтому одного глобального ограничения мало: разумнее иметь несколько политик, более строгие на чувствительных и «дорогих» путях.

Ключевой вопрос реализации — по какому ключу считать. Лимит только по IP легко обходится сменой адреса и при этом бьёт по добропорядочным пользователям за общим NAT (например, за корпоративным шлюзом, где сотни людей выходят с одного адреса). Лимит по идентификатору клиента или токену точнее для аутентифицированных запросов, но для анонимных эндпоинтов выбора, кроме IP, часто нет. На практике комбинируют ключи и добавляют более мягкое поведение, чем жёсткий отказ: возвращают 429 с заголовком Retry-After, чтобы корректный клиент знал, когда повторить, а не считал сервис сломанным.

Авторизация на каждый объект и каждое действие

Главные риски API Top 10 — Broken Object Level Authorization (тот же IDOR: вернули чужой объект по id) и Broken Function Level Authorization (обычный пользователь дёрнул админский эндпоинт). Лекарство — проверять права на сервере на каждый объект и каждую функцию, deny by default. Это прямое продолжение раздела про авторизацию.

// Безопасно: проверка владения объектом в API-обработчике
function getInvoice(id, user) {
  const inv = db.invoices.find(id);
  if (!inv || inv.ownerId !== user.id) throw forbidden();  // не своё -> 403
  return inv;
}

Строгая валидация и минимизация данных

Принимайте только описанные схемой поля и типы, отвергайте лишнее (защита от mass assignment, когда клиент дописывает в объект поле role: admin). И отдавайте только нужное: не сериализуйте сущность целиком — excessive data exposure происходит, когда API возвращает поля вроде хеша пароля или внутренних флагов.

// Уязвимо: принимаем и возвращаем объект целиком
user = User(request.body);          // клиент мог прислать isAdmin=true (mass assignment)
return user;                         // в ответе утечёт passwordHash, внутренние поля

// Безопасно: явный allowlist полей на вход и на выход (DTO)
user = User(name=body.name, email=body.email);          // только разрешённые поля
return { id: user.id, name: user.name, email: user.email };  // только публичные

Как работает под капотом: контракт API как контроль

Описание API (OpenAPI-схема) — это не только документация, но и инструмент безопасности: по нему генерируют валидацию запросов/ответов, отвергают неизвестные поля и проверяют типы централизованно. Чёткий контракт + DTO на вход и выход закрывают сразу несколько рисков API Top 10: mass assignment, excessive data exposure и часть инъекций через типизацию.

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

Та же схема описывает и форму ответа, и здесь рождается защита от excessive data exposure. Если ответ собирается из явного DTO, а не из сериализации модели целиком, то новое внутреннее поле, добавленное в таблицу пользователей (скажем, технический флаг или хеш), физически не попадёт в выдачу — его просто нет в контракте ответа. Это снимает целый класс тихих утечек, которые возникают, когда кто-то расширяет модель данных и забывает, что она напрямую улетает клиенту. Ниже видно, как опасно сериализовать сущность целиком и как DTO делает контракт явным.

Частые ошибки

  • Нет rate limiting. Перебор и DoS ничем не сдержаны.
  • Объект по id без проверки владельца. BOLA/IDOR — топ-риск API.
  • Принимать и отдавать сущность целиком. Mass assignment и утечка лишних полей.
  • Скрытый админ-эндпоинт «и так никто не найдёт». Нужна проверка прав, а не безвестность.
  • Rate limit только по IP. Обходится сменой адреса и бьёт по пользователям за общим NAT; комбинируйте ключи.
  • Доверие к клиенту. Скрытые поля и отключённые кнопки не защищают: к API ходят напрямую, минуя интерфейс.

Итоги

  • API легко атаковать массово: ставьте rate limiting и квоты.
  • Топ-риски — про авторизацию: проверяйте права на каждый объект и функцию, deny by default.
  • Валидируйте вход схемой, отдавайте только нужные поля (DTO), отвергайте лишнее.
Проверьте себя
1. Зачем API нужен rate limiting?
AЧтобы экономить электроэнергию
BЧтобы сдержать массовый перебор, скрейпинг и отказ в обслуживании, ведь к API легко обращаться в цикле
CЧтобы ускорить ответы
DЧтобы шифровать трафик
2. Что такое Broken Object Level Authorization (BOLA) в API Top 10?
AУтечка ключа шифрования
BВозврат чужого объекта по его id без проверки, что он принадлежит запросившему (по сути IDOR)
CОтсутствие HTTPS
DСлишком длинные токены
3. Как защититься от mass assignment и утечки лишних полей в API?
AПринимать и возвращать сущность целиком
BИспользовать явный allowlist полей на вход и DTO с публичными полями на выход, отвергая неизвестные поля
CШифровать весь ответ
DОтключить валидацию для скорости