Безопасность API: OWASP API Security Top 10 на практике
Защита REST и GraphQL API по OWASP API Security Top 10 (2023): авторизация на уровне объектов и свойств, аутентификация, ограничение ресурсов, SSRF, конфигурация, валидация данных, мониторинг и чек-лист.
API — это то место, где приложение открыто внешнему миру напрямую. Интерфейс сайта или мобильного приложения ограничивает, какие действия доступны пользователю, но злоумышленник обращается к API в обход интерфейса: подставляет чужие идентификаторы, отправляет лишние поля, перебирает запросы с огромной скоростью и вызывает методы, до которых в интерфейсе нет кнопки. Большинство серьёзных инцидентов с API связаны не с экзотическими атаками, а с базовыми ошибками авторизации и отсутствием ограничений. Ниже — практическое руководство по защите REST и GraphQL API: разбор списка рисков OWASP API Security Top 10 в редакции 2023 года с примерами уязвимого и исправленного кода, а также аутентификация, валидация данных, ограничение частоты запросов, заголовки безопасности, журналирование и чек-лист для проверки собственного API. OWASP API Security Top 10 OWASP — открытое сообщество, которое публикует рейтинги наиболее распространённых рисков безопасности. Отдельный список для API в редакции 2023 года выглядит так. API1: Broken Object Level Authorization — нарушение авторизации на уровне объектов. API2: Broken Authentication — ошибки аутентификации. API3: Broken Object Property Level Authorization — нарушение авторизации на уровне свойств объекта: раскрытие лишних полей и массовое присвоение. API4: Unrestricted Resource Consumption — неограниченное потребление ресурсов. API5: Broken Function Level Authorization — нарушение авторизации на уровне функций. API6: Unrestricted Access to Sensitive Business Flows — неограниченный доступ к чувствительным бизнес-процессам. API7: Server Side Request Forgery — подделка запросов со стороны сервера. API8: Security Misconfiguration — ошибки конфигурации. API9: Improper Inventory Management — неучтённые версии и эндпоинты. API10: Unsafe Consumption of APIs — небезопасное использование сторонних API. API1: авторизация на уровне объектов Самая частая и самая опасная уязвимость. Пользователь аутентифицирован, но сервер не проверяет, принадлежит ли ему запрашиваемый объект. // Уязвимо: любой авторизованный пользователь получит любой заказ, // перебирая идентификаторы app.get('/api/orders/:id', auth, async (req, res) = { const order = await db.orders.findById(req.params.id); res.json(order); }); // Исправлено: объект ищется в пределах прав пользователя app.get('/api/orders/:id', auth, async (req, res) = { const order = await db.orders.findOne({ id: req.params.id, clientId: req.user.clientId, }); if (!order) return res.status(404).end(); res.json(order); }); Правила: проверка владения объектом выполняется на сервере в каждом обработчике или в общем слое доступа к данным; для чужого объекта возвращается 404, чтобы не подтверждать его существование; непредсказуемые идентификаторы вроде UUID затрудняют перебор, но не заменяют проверку прав. API2: аутентификация ограничение попыток входа, сброса пароля и проверки кодов подтверждения; хеширование паролей специальными функциями — Argon2id или bcrypt; токены с коротким сроком жизни и возможностью отзыва; проверка подписи, алгоритма, срока действия и получателя JWT; повторная аутентификация для чувствительных операций: смены email, пароля, платёжных данных; токены не передаются в адресе запроса, где они попадают в журналы и историю браузера. Подробно о паролях, токенах и хранении секретов — в статье о шифровании и криптографии в веб-приложениях . API3: авторизация на уровне свойств Раскрытие лишних данных API возвращает объект целиком, рассчитывая, что интерфейс покажет только нужное. Но ответ API видит любой, кто откроет инструменты разработчика. // Уязвимо: в ответе хеш пароля, внутренние флаги, служебные поля res.json(user); // Исправлено: явный список полей для конкретного сценария res.json({ id: user.id, name: user.name, avatarUrl: user.avatarUrl }); Массовое присвоение Сервер копирует все поля из тела запроса в объект — и пользователь добавляет поле role: 'admin' или balance . // Уязвимо await db.users.update(req.user.id, req.body); // Исправлено: схема с разрешёнными полями const UpdateProfile = z.object({ name: z.string().min(1).max(100), phone: z.string().regex(/^\+7\d{10}$/).optional(), }).strict(); // лишние поля — ошибка const data = UpdateProfile.parse(req.body); await db.users.update(req.user.id, data); API4: неограниченное потребление ресурсов API без ограничений позволяет исчерпать ресурсы сервера, базы данных или бюджет платных сторонних сервисов: отправку SMS, запросы к языковым моделям, генерацию отчётов. ограничение частоты запросов по пользователю, ключу API и IP-адресу, с разными лимитами для разных операций; ограничение размера тела запроса, загружаемых файлов и числа элементов в пакетных операциях; обязательная пагинация с максимальным размером страницы; тайм-ауты на запросы к базе данных и внешним сервисам; квоты на дорогостоящие операции. import rateLimit from 'express-rate-limit'; app.use('/api/', rateLimit({ windowMs: 60_000, limit: 300 })); app.use('/api/auth/login', rateLimit({ windowMs: 15 * 60_000, limit: 10 })); app.use('/api/sms/send', rateLimit({ windowMs: 60 * 60_000, limit: 5 })); app.use(express.json({ limit: '100kb' })); При нескольких экземплярах приложения счётчики хранят во внешнем хранилище, например в Redis, иначе каждый экземпляр считает отдельно. API5: авторизация на уровне функций Административные методы доступны обычным пользователям, потому что их просто нет в интерфейсе, а сервер не проверяет роль. function requireRole(...roles) { return (req, res, next) = roles.includes(req.user?.role) ? next() : res.status(403).end(); } app.delete('/api/users/:id', auth, requireRole('admin'), deleteUser); Надёжнее модель «запрещено всё, что явно не разрешено»: каждый новый маршрут по умолчанию требует явного указания прав. API6: чувствительные бизнес-процессы Технически корректный API может использоваться во вред: автоматическая скупка товара с ограниченным количеством, массовая регистрация для получения бонусов, перебор промокодов, спам через форму обратной связи. Защита строится на понимании процесса: ограничения на уровне сценария, выявление автоматизации, подтверждения, проверка устройств и поведения — для тех операций, где злоупотребление приносит выгоду. API7: SSRF Если API загружает данные по адресу, переданному пользователем, — картинку по ссылке, веб-хук, импорт файла, — злоумышленник может заставить сервер обратиться к внутренним ресурсам: служебным адресам метаданных облака, внутренним сервисам, административным панелям. разрешать только нужные протоколы и, по возможности, список доверенных доменов; проверять адрес после разрешения DNS и запрещать внутренние и служебные диапазоны; не следовать перенаправлениям автоматически или проверять каждое; выполнять такие запросы из изолированного сервиса без доступа к внутренней сети. API8: ошибки конфигурации CORS: разрешать только известные источники; сочетание разрешения любого источника с передачей учётных данных недопустимо. Заголовки безопасности и HTTPS везде. Сообщения об ошибках без стеков вызовов, SQL-запросов и версий компонентов в продакшене. Отключённые отладочные режимы , документация и консоли в продакшене — только при необходимости и под авторизацией. Обновлённые зависимости и регулярная проверка на известные уязвимости. import helmet from 'helmet'; import cors from 'cors'; app.use(helmet()); app.use(cors({ origin: ['https://app.example.ru'], credentials: true, })); API9: учёт версий и эндпоинтов Старая версия API, которую забыли отключить, тестовый стенд с продуктивными данными, открытый служебный эндпоинт — всё это остаётся доступным и уязвимым, пока о нём никто не помнит. актуальная спецификация всех API, например в формате OpenAPI; план вывода старых версий из эксплуатации; тестовые окружения без продуктивных данных и закрытые от внешнего доступа; регулярная инвентаризация открытых наружу адресов. API10: небезопасное использование сторонних API Данные, полученные от партнёрского или стороннего API, так же недоверенные, как данные от пользователя. Их нужно валидировать, соблюдать тайм-ауты, использовать шифрованные соединения, не следовать перенаправлениям вслепую и учитывать, что сторонний сервис может быть скомпрометирован. Отдельный риск — языковые модели: текст, полученный от модели, может содержать внедрённые инструкции и не должен напрямую превращаться в команды или запросы к базе данных. Валидация входных данных Проверка всех данных на границе системы — параметров пути, строки запроса, тела и заголовков — по строгой схеме. Схема определяет типы, форматы, длины и допустимые значения и отклоняет лишние поля. От SQL-инъекций защищают параметризованные запросы или ORM, а не ручное экранирование: // Уязвимо db.query(`SELECT * FROM users WHERE email = '${email}'`); // Безопасно db.query('SELECT * FROM users WHERE email = $1', [email]); Особенности GraphQL Глубина и сложность запросов. Один вложенный запрос может потребовать огромных вычислений. Нужны ограничения глубины, оценка сложности и тайм-ауты. Интроспекция в продакшене раскрывает всю схему; её стоит отключать или ограничивать. Авторизация на уровне полей и резолверов , а не только на уровне запроса. Пакетные запросы и псевдонимы позволяют обойти ограничение частоты: сотни попыток в одном HTTP-запросе. Лимиты нужно считать по операциям, а не по запросам. Сохранённые запросы для публичных клиентов — сервер выполняет только заранее разрешённые операции. Выбор между REST и GraphQL мы разбирали в статье о GraphQL и REST . Журналирование и мониторинг фиксировать аутентификацию, отказы в доступе, изменения прав и чувствительные операции; не записывать в журналы пароли, токены, полные номера карт и лишние персональные данные; настроить оповещения: всплеск ответов 401 и 403, перебор идентификаторов, аномальный объём выгрузок, запросы из необычных мест; хранить журналы так, чтобы их нельзя было изменить из скомпрометированного приложения. Как построить наблюдаемость системы, — в статье о мониторинге и observability . Тестирование безопасности API Автоматические тесты авторизации: для каждого эндпоинта проверка, что пользователь не получает чужие объекты и не вызывает функции другой роли. Это самые ценные тесты безопасности, и их удобно писать вместе с функциональными. Сканеры уязвимостей — например, OWASP ZAP — в конвейере сборки. Проверка зависимостей на известные уязвимости. Ручной анализ и тестирование на проникновение перед запуском критичных систем и после крупных изменений. Чек-лист Каждый эндпоинт проверяет права на конкретный объект. Административные функции проверяют роль на сервере; доступ запрещён по умолчанию. Ответы содержат только нужные поля; входящие данные валидируются строгой схемой. Ограничения частоты, размеров, пагинации и тайм-ауты настроены. Аутентификация с ограничением попыток, короткими токенами и безопасным хранением. Параметризованные запросы к базе данных. CORS разрешает только известные источники; заголовки безопасности и HTTPS. Ошибки не раскрывают внутренние детали. Все версии и эндпоинты учтены; старые отключены. Данные сторонних API и языковых моделей проверяются как недоверенные. Журналы и оповещения о подозрительной активности настроены. Тесты авторизации входят в автоматические проверки. Защищённые API и серверную часть мы проектируем в рамках разработки веб-проектов и мобильных приложений . Частые вопросы Достаточно ли HTTPS и токена для безопасности API? Нет. HTTPS защищает передачу данных, токен подтверждает, кто делает запрос. Самые частые уязвимости — отсутствие проверки, может ли этот пользователь получить именно этот объект или выполнить именно эту операцию. Скрывает ли UUID вместо числовых идентификаторов уязвимость BOLA? Затрудняет перебор, но не устраняет проблему: идентификаторы попадают в ссылки, журналы и ответы других запросов. Проверка прав на объект нужна всегда. Нужен ли API-шлюз? Шлюз удобен для централизованной аутентификации, ограничения частоты, журналирования и маршрутизации, особенно при множестве сервисов. Но авторизацию на уровне объектов и бизнес-правила он не заменяет — они остаются в сервисах.