Защита 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), отвергайте лишнее.