Базовая безопасность для API: авторизация, CORS, rate limiting и защита от повторов
Соберём чек-лист практик для защиты веб-сервисов и разберём типовые уязвимости на уровне конфигурации и логики.
Содержание
Базовая безопасность для API: авторизация, CORS, rate limiting и защита от повторов
API редко ломают «хакерскими магиями». Чаще это последствия типовых решений на уровне конфигурации и бизнес-логики: неверно настроенная авторизация, слишком доверчивые CORS-политики, отсутствие ограничений частоты, а также повторяемые запросы без идемпотентности. В результате злоумышленник получает либо прямой доступ к данным, либо возможность нагрузить сервис, либо обойти целевые ограничения.
Ниже — системный чек-лист базовых практик безопасности для веб-сервисов и разбор самых распространённых уязвимостей, связанных с авторизацией, CORS, rate limiting и защитой от повторов. Цель — чтобы вы могли привести API к более устойчивому состоянию ещё до глубокого аудита.
1) Авторизация: от аутентификации к модели доступа
1.1. Аутентификация ≠ авторизация
Аутентификация отвечает на вопрос: «кто это?». Обычно это токен (JWT), сессия, API-ключ, mTLS и т.д.
Авторизация — «что он может?». Это проверка прав на ресурс и действие: RBAC/ABAC, scope/role, ACL.
Типичная ошибка: разработчики проверяют только наличие токена и считают, что «токен значит всё разрешено». На практике нужен слой проверок прав на конкретный эндпоинт и объект (tenant, user, project, order…).
1.2. Принцип “default deny” и точные проверки
Базовое правило: если вы не можете объяснить, почему запрос разрешён, — он должен быть запрещён.
Практика:
- Проверяйте права до выполнения бизнес-логики.
- Проверяйте права на конкретный ресурс, а не “в целом для пользователя”.
- Делайте единый слой авторизации (policy layer), а не копируйте проверки по контроллерам.
Пример на псевдокоде:
// пример идеи: policy/check до выполнения операции
function can(user, action, resource) {
// default deny
if (!user) return false;
// tenant isolation
if (resource.tenantId !== user.tenantId) return false;
// action checks
if (action === 'read:order') return user.roles.includes('support') || user.roles.includes('admin');
if (action === 'write:order') return user.roles.includes('admin');
return false;
}
Важный нюанс: авторизацию нельзя воспринимать как статическую конфигурацию. Политики должны быть согласованы с бизнес-правилами.
1.3. Bypass через “объектные” уязвимости (IDOR)
Классика: клиент передаёт orderId, сервер извлекает объект по ID и не проверяет, что у пользователя есть доступ.
IDOR (Insecure Direct Object Reference) часто выглядит безобидно:
/api/orders/123возвращает заказ 123- атакующий перебирает ID
- получает чужие данные
Защита:
- Проверка доступа после выборки объекта.
- Или выборка только в рамках домена пользователя:
WHERE order.tenantId = user.tenantId.
1.4. Скоупы и “тонкий” JWT
JWT полезен, но опасен при неправильной настройке:
- Скоупы/claims могут быть слишком широкими («всё и сразу»).
- Токен может быть использован для действий, для которых он не предназначен.
- Сервер может полагаться на claim “role” без проверки подписи/актуальности.
Практический ориентир:
- Используйте минимально достаточные claims.
- На сервере проверяйте подпись токена (и алгоритм).
- Согласуйте жизнь токена и необходимость отзыва (revocation).
- Для критичных операций иногда нужна re-auth или server-side проверка актуального статуса пользователя.
1.5. Ошибки статуса и сообщений
Не выдавайте злоумышленнику подсказок:
- Не различайте “нет токена” и “токен неверный” в сообщениях.
- В логах фиксируйте детали, в ответах — нейтральные формулировки.
- Следите за корректными HTTP-кодами:
401 Unauthorized— нет/плохо аутентифицировали403 Forbidden— аутентификация есть, но прав недостаточно
2) CORS: когда “разрешить фронту” превращается в дыру
CORS — политика браузера. Она не защищает сервер напрямую, но существенно влияет на то, какие запросы сможет сделать вредоносный сайт из браузера жертвы.
2.1. Что реально делает CORS
Браузер при кросс-доменном запросе проверяет ответ сервера:
Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-HeadersAccess-Control-Allow-Credentials
Если политика слишком либеральная — любой сайт сможет получать ответы из вашего API при наличии у жертвы cookies/credentials.
2.2. Главный анти-паттерн: * с credentials
Нельзя делать одновременно:
Access-Control-Allow-Origin: *Access-Control-Allow-Credentials: true
Браузер это заблокирует, но в некоторых реализациях возможны неожиданные эффекты. В целом безопасная стратегия — не использовать wildcard, если включены credentials.
Более правильный подход:
- Валидация
Originи возврат конкретного домена из списка. - Либо отключать credentials, если они не нужны.
2.3. Строгость по методам и заголовкам
Частая ошибка — разрешить всё:
Access-Control-Allow-Methods: *(или список “на всякий случай”)- широкий
Access-Control-Allow-Headers, включая чувствительные.
Что важно:
- Разрешайте только те заголовки, которые реально нужны (например
Authorization,Content-Type). - Ограничивайте методы (
GET, POST, PUT, DELETE) по назначению.
2.4. Preflight: атака через сложность
Когда запрос не “простого типа”, браузер делает preflight OPTIONS. Если вы игнорируете безопасность на OPTIONS, но разрешаете слишком широко, вы открываете возможность для нежелательных запросов.
Чек-лист:
- Обрабатывайте
OPTIONSтак же строго, как основной метод. - Применяйте авторизацию и ограничения (например rate limiting) и к preflight, если это критично.
2.5. Практический шаблон конфигурации
Пример Express (Node.js), который аккуратно валидирует Origin:
import express from "express";
const app = express();
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com",
]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && allowedOrigins.has(origin)) {
res.header("Access-Control-Allow-Origin", origin);
res.header("Vary", "Origin");
res.header("Access-Control-Allow-Credentials", "true");
res.header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
res.header("Access-Control-Allow-Headers", "Authorization,Content-Type");
}
// иначе: не добавляем CORS заголовки
next();
});
app.options("*", (req, res) => res.sendStatus(204));
Ключевой момент: Vary: Origin — чтобы прокси/кэш не “подменяли” ответ между доменами.
3) Rate limiting: защита от перебора, злоупотребления и “тихого” DoS
Rate limiting — одна из самых эффективных мер в базовой безопасности. Она не отменяет уязвимость, но уменьшает ущерб и делает эксплуатацию медленной и дорогой.
3.1. Что лимитировать и по каким ключам
Нужно выбрать ось лимитирования. Типовые ключи:
- по IP (
X-Forwarded-Forаккуратно, учитывая прокси) - по userId (после авторизации)
- по API-key / token id
- по комбинации (user + IP, или token + endpoint)
Ошибки:
- Лимит только по IP: при NAT множество легитимных пользователей могут “толкаться”.
- Лимит только по userId: злоумышленник может использовать множество аккаунтов (credential stuffing / account creation).
Практика: используйте разные лимиты для разных уровней:
- общий лимит на IP/подсеть
- отдельный лимит на userId или токен
- отдельный лимит на “дорогие” эндпоинты (например поиск по графу, генерация отчётов)
3.2. Границы, которые имеют смысл
С точки зрения эксплуатации важно:
- Учитывать специфичность: login/refresh — обычно требует жёстче лимитирования, чем public GET.
- Установить лимиты по времени: per minute/per 5 minutes/per day.
- Определить поведение при превышении:
429 Too Many Requests.
Пример (условный) политики:
/auth/login: 5 запросов / 10 минут / IP+username/api/orders/{id}: 60 запросов / минуту / userId/api/search: 30 запросов / минуту / userId+queryHash (если есть риск тяжелых запросов)
3.3. Алгоритм: fixed window, sliding window, token bucket
Классические подходы:
- Fixed window: счётчик на фиксированном интервале (прост, но уязвим к “пилению по границе”)
- Sliding window: точнее, но сложнее
- Token bucket / leaky bucket: сглаживает всплески и удобен для QoS
Если вы используете готовые библиотеки — проверьте, какой именно алгоритм они применяют.
3.4. Не забыть про разные статусы
Rate limiting должен быть одинаково полезен:
- Для
5xxиногда имеет смысл лимитировать иначе (или хотя бы логировать). - Для
4xxлогика может различаться: частые401/403могут быть признаком атаки.
Но слепо лимитировать любые ошибки опасно — можно заблокировать легитимных клиентов при ошибках конфигурации токенов.
3.5. Пример middleware с Redis (идея)
Псевдо-реализация (важно: реальный код зависит от выбранного пакета):
import rateLimit from "some-rate-limit-lib";
const limiter = rateLimit({
store: "redis",
points: 60,
durationMs: 60 * 1000,
keyGenerator: (req) => {
const userId = req.user?.id;
return userId ? `user:${userId}` : `ip:${req.ip}`;
},
onLimitExceeded: (req, res) => res.status(429).json({ error: "rate_limited" }),
});
app.use("/api", limiter);
Главное — не забывать, что лимит должен быть эффективен в распределенной среде (несколько инстансов). Поэтому обычно нужен единый store: Redis, Memcached, etc.
4) Защита от повторов: идемпотентность и replay-атаки
Повторы могут быть как случайными (ретраи клиента из-за тайм-аутов), так и злоумышленными (replay атаки на запросы, особенно если есть токены длительного действия и отсутствие защиты на уровне запросов).
4.1. Идемпотентность как базовая защита
Если операция “создать заказ” или “списать оплату” выполняется повторно, последствия критичны. Поэтому для операций, которые можно безопасно переисполнить, нужен механизм идемпотентности.
Концепция: клиент отправляет ключ идемпотентности, сервер:
- выполняет операцию один раз,
- а при повторе того же ключа возвращает тот же результат, а не повторяет действие.
Пример: заголовок Idempotency-Key: <uuid>.
Серверная схема:
- Проверить наличие
Idempotency-Key - Привязать ключ к пользователю/ресурсу (или хотя бы userId)
- Хранить результат и/или статус операции
- При повторе вернуть прежний результат
4.2. Что хранить: статус, результат, время
Достаточно хранить:
- статус (например
processing/success/failed) - payload результата (или ссылку на запись)
- timestamp и TTL (например 24 часа, 7 дней — по бизнес требованиям)
Нельзя бесконечно хранить: это превращается в утечку хранилища.
4.3. Replay через старые токены
Если у вас есть access token с длительным сроком, злоумышленник может перехватить запрос и воспроизвести его. Защита:
- не только CORS/rate limiting
- но и идемпотентность для опасных операций
- короткое время жизни токенов
- правильное использование nonce/challenge в сценариях критичных транзакций
Для API на уровне REST обычно достаточно idempotency keys и корректных проверок на стороне сервера.
4.4. Подводные камни при внедрении идемпотентности
-
Ключ без привязки к пользователю
Если ключ глобальный, кто-то может “подглянуть” ключ (или подобрать) и получить чужой результат. Поэтому идемпотентность должна быть в контексте пользователя/tenant. -
Ключ только для POST, но не для всего критичного
Идемпотентность нужна не только POST. Всё зависит от бизнес-операций. Например, “подтвердить действие” может быть PUT/POST. -
Пересчёт результата при повторе
Нужно возвращать тот же результат (или гарантировать эквивалентность). Иначе повтор превращается в конкурентное условие.
4.5. Пример логики идемпотентности на уровне БД
Упрощённая иллюстрация для SQL (идея):
-- таблица для идемпотентности
CREATE TABLE idempotency_keys (
key_hash TEXT PRIMARY KEY,
user_id UUID NOT NULL,
route TEXT NOT NULL,
request_body_hash TEXT NOT NULL,
status TEXT NOT NULL,
response_json JSONB,
created_at TIMESTAMP NOT NULL DEFAULT now(),
updated_at TIMESTAMP NOT NULL DEFAULT now()
);
В приложении:
- считаете
key_hashизIdempotency-Key - проверяете существование записи
- если есть
success— возвращаетеresponse_json - если
processing— можно вернуть 409/425/или ждать, зависит от UX - если нет — создаёте транзакцию/замок и выполняете операцию
Критично: делайте атомарность на уровне БД (уникальный индекс/транзакция), а не “проверил — сделал”.
5) “Базовый” чек-лист: что стоит проверить в первую очередь
Ниже — практический список, который обычно даёт максимальный эффект при минимальной цене исправлений.
5.1. Авторизация и доступ
- есть модель доступа (RBAC/ABAC) и проверки “default deny”
- нет IDOR: ресурс проверяется в контексте tenant/user
- единый слой авторизации (или middleware), а не копипаста
- правильные статусы: 401 vs 403
- чувствительные детали ошибок не утекают в клиентские ответы
5.2. CORS
- CORS включён только для доверенных
Origin - нет
*вместе сAccess-Control-Allow-Credentials: true - корректные
Vary: Origin - preflight
OPTIONSобработан строго - разрешены только нужные методы и заголовки
5.3. Rate limiting
- лимит на “публичные” и “опасные” эндпоинты различается
- ключ лимитирования учитывает user/token после аутентификации
- есть единый store (Redis) для распределённых инстансов
- ответ при превышении —
429и разумная политика заголовков (напримерRetry-After) - логирование и метрики (сколько 429, какие endpoints)
5.4. Повторы и идемпотентность
- для операций с побочными эффектами (создание/оплата/отправка) есть idempotency key
- ключ привязан к пользователю/tenant и по возможности к route
- атомарность на уровне БД
- хранится результат/статус и есть TTL
- ретраи клиентов не приводят к двойным списаниям
6) Типовые сценарии атак и как на них реагировать
6.1. “У меня есть токен — значит, можно всё”
Симптом: эндпоинт возвращает чужие ресурсы при подстановке ID.
Причина: нет объектной проверки прав.
Фикс: проверка доступа к конкретному ресурсу или выборка с учётом tenant/user.
6.2. “Пускай CORS будет широким — так проще фронту”
Симптом: вредоносный сайт может делать запросы из браузера и читать ответы от API.
Причина: Access-Control-Allow-Origin возвращает слишком много вариантов.
Фикс: whitelist origin, ограничение заголовков, Vary: Origin.
6.3. “Лимитов нет — будем посмотреть”
Симптом: 10–100x рост нагрузки на конкретные эндпоинты, растущие latency и расходы.
Причина: отсутствует rate limiting или он слишком мягкий/не тем ключом.
Фикс: rate limits по endpoint и ключу (user/token + IP), отдельные лим
Комментарии
Пока нет комментариев