Безопасность API: OWASP, CORS, HTTPS

API — это дверь в ваши данные; безопасность решает, кого и куда она пускает.

OWASP API Security Top 10 — список самых частых и опасных уязвимостей API, составленный сообществом OWASP как ориентир для защиты.

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

OWASP API Security Top 10: ключевые угрозы

УгрозаСуть
BOLA (Broken Object Level Authorization)сервер не проверяет, что объект принадлежит запросившему: GET /orders/777 отдаёт чужой заказ
Broken Authenticationслабая проверка личности: предсказуемые токены, отсутствие истечения, утечка ключей
Excessive Data Exposureответ отдаёт лишние поля (хэш пароля, внутренние флаги), полагаясь на то, что «фронт их не покажет»
Mass Assignmentклиент шлёт лишние поля ("role":"admin"), и сервер слепо их сохраняет
Lack of Rate Limitingнет защиты от перебора и флуда (см. урок про rate limiting)

BOLA — угроза номер один: чаще всего ломаются именно из-за неё. Пример атаки — подстановка чужого id:

# Токен мой, но прошу чужой заказ
curl https://api.example.com/v1/orders/777 \
  -H "Authorization: Bearer <my_token>"

Если сервер не проверит order.owner == current_user, он отдаст чужие данные. Лечится проверкой владения на каждом доступе к объекту, а не только наличием валидного токена.

Excessive data exposure и mass assignment

Эти две угрозы зеркальны. Первая — лишнее наружу: API возвращает весь объект из БД, включая password_hash и is_admin, надеясь, что клиент их «не отрендерит». Решение — отдавать только явно перечисленные поля (whitelist в сериализаторе). Вторая — лишнее внутрь: клиент дописывает в тело то, чего там быть не должно:

{
  "name": "Иван",
  "email": "[email protected]",
  "role": "admin"
}

Если сервер делает «сохрани всё, что прислали», обычный пользователь сделает себя админом. Защита — принимать только разрешённый набор полей, а role и подобные — никогда из тела клиента.

Отдельного внимания заслуживает broken authentication — слабая проверка личности. Сюда относятся предсказуемые или короткие токены, отсутствие срока истечения, передача учётных данных в URL (они попадают в логи и историю браузера), приём слабых паролей и отсутствие защиты от перебора. Признак здоровой аутентификации — токены достаточной длины и случайности, обязательное истечение, защита эндпоинта логина rate-лимитом и единый, не раскрывающий деталей ответ на ошибку входа (не «нет такого пользователя» против «неверный пароль» — это подсказывает атакующему, какие логины существуют).

Валидация входных данных

Главный принцип: не доверять клиенту вообще. Любой вход проверяется на сервере: типы, диапазоны, длины, форматы, обязательность. Клиентская валидация — лишь удобство для пользователя, обойти её тривиально (тот же curl). Сервер обязан:

  • проверять типы и форматы (email — это email, age — целое 0..150);
  • отвергать неизвестные/лишние поля или игнорировать их;
  • экранировать данные перед записью в БД (против инъекций) и перед выводом;
  • на невалидный вход возвращать 400 Bad Request с понятным описанием.

HTTPS обязателен

Весь трафик API должен идти по HTTPS (TLS). По обычному HTTP заголовки, тело и токены передаются открытым текстом — любой на пути (публичный Wi-Fi, провайдер) прочитает Authorization: Bearer <token> и угонит сессию. TLS шифрует канал и подтверждает подлинность сервера. На проде HTTP-эндпоинтов быть не должно: запросы по HTTP либо отклоняют, либо редиректят на HTTPS, а заголовок Strict-Transport-Security заставляет браузер всегда ходить только по HTTPS.

CORS: безопасные кросс-доменные запросы

Браузер по умолчанию запрещает скрипту с одного origin (схема+домен+порт) читать ответы API на другом origin — это Same-Origin Policy, защита от того, чтобы зловредный сайт дёргал ваш API от имени залогиненного пользователя. CORS (Cross-Origin Resource Sharing) — механизм, которым сервер явно разрешает конкретным origin обращаться к нему.

Для «непростых» запросов (методы вроде PUT/DELETE, кастомные заголовки) браузер сначала шлёт preflight — предварительный запрос OPTIONS, спрашивая разрешение:

Браузер                                  Сервер API
   |  OPTIONS /v1/orders                     |
   |  Origin: https://app.example.com        |
   |  Access-Control-Request-Method: DELETE  |
   |---------------------------------------->|
   |<----------------------------------------|
   |  204 No Content                         |
   |  Access-Control-Allow-Origin: https://app.example.com
   |  Access-Control-Allow-Methods: GET, POST, DELETE
   |  Access-Control-Allow-Headers: Authorization, Content-Type
   |                                         |
   |  (preflight ok) DELETE /v1/orders/5     |
   |---------------------------------------->|
   |<------------- 200 OK --------------------|

Ключевой заголовок ответа — Access-Control-Allow-Origin: он перечисляет, какому origin браузер позволит прочитать ответ. Если в нём не ваш сайт — браузер заблокирует чтение, даже если сервер данные вернул.

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400

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

CORS — это исключительно браузерная защита: проверку делает сам браузер, сравнивая Origin запроса с тем, что разрешил сервер в Access-Control-Allow-Origin. Сервер при запрете всё равно может вернуть данные — но браузер не отдаст их JavaScript. Поэтому CORS не защищает API от curl или серверных клиентов (у них нет Same-Origin Policy); он лишь регулирует, какие сайты в браузере могут читать ваш ответ. Access-Control-Max-Age кэширует результат preflight, чтобы браузер не слал OPTIONS перед каждым запросом. Распространённая опасная практика — Access-Control-Allow-Origin: * вместе с куками: браузер это запрещает, а для приватных API звёздочка вообще недопустима.

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

  • Проверять токен, но не проверять владение объектом — это BOLA, главная дыра.
  • Отдавать весь объект из БД, включая хэши и служебные флаги.
  • Сохранять все присланные поля скопом — открывает mass assignment (role: admin).
  • Полагаться на клиентскую валидацию — её обходят за секунду.
  • Думать, что CORS защищает данные: он защищает только браузерных читателей, не curl.
  • Ставить Access-Control-Allow-Origin: * на приватный API.
  • Разрешать HTTP-эндпоинты в проде — токены утекают открытым текстом.

Итоги

  • OWASP API Top 10 — карта главных угроз; BOLA, broken auth, excessive data exposure, mass assignment, отсутствие rate limiting в верхушке.
  • BOLA лечится проверкой владения объектом при каждом доступе.
  • Не доверяй клиенту: валидируй вход на сервере, ограничивай поля и на входе, и на выходе.
  • HTTPS обязателен — иначе токены и данные идут открыто.
  • CORS — браузерный механизм: Access-Control-Allow-Origin решает, какой origin прочитает ответ; «непростые» запросы предваряет preflight OPTIONS.
  • CORS не заменяет авторизацию и не защищает от не-браузерных клиентов.
Проверьте себя
1. Что такое уязвимость BOLA?
Aсервер не шифрует трафик
Bсервер не проверяет, что объект принадлежит запросившему пользователю
Cклиент присылает слишком много запросов
Dтокен не имеет срока истечения
2. Почему серверная валидация входных данных обязательна, даже если есть клиентская?
Aклиентская работает медленнее
Bклиентскую валидацию легко обойти, отправив запрос напрямую (например, curl)
Cсервер не умеет проверять типы
Dэто требование CORS
3. Что делает браузер перед "непростым" кросс-доменным запросом (например, DELETE)?
Aотправляет preflight-запрос OPTIONS, спрашивая разрешение
Bшифрует тело дважды
Cблокирует запрос навсегда
Dменяет метод на GET
4. Защищает ли CORS API от запросов через curl?
Aда, CORS блокирует любые внешние запросы
Bнет, CORS — браузерный механизм и не действует на curl и серверные клиенты
Cда, но только при HTTPS
Dнет, CORS вообще не связан с доступом