javascript

Жизненный цикл API-токенов: От генерации до компрометации

  • вторник, 14 июля 2026 г. в 00:00:21
https://habr.com/ru/articles/1058318/

Введение

В 2026 году статистика утечек данных выглядит пугающе. По данным отчётов инцидентов, публикуемых крупными вендорами безопасности, кража сессионных токенов через XSS-атаки занимает третье место среди всех векторов компрометации, уступая только фишингу и атакам на цепочку поставок. И это не просто какие-то там аккаунты в соцсетях. Мы говорим о корпоративных аккаунтах с доступом к CRM, финансовым отчетам и корпоративной почте. Худшее в этой истории то, что такие кражи обнаруживают в среднем через 43 дня. Это почти полтора месяца, в течение которых злоумышленник чувствует себя в вашей системе как у себя дома.

А сама частота XSS атак уверенно держит пятое место вместе со всеми остальными видами инъекций.

Топ 10 за 2025 год. С 2021 года XSS входят в категорию Injections.
Топ 10 за 2025 год. С 2021 года XSS входят в категорию Injections.

В сообществе разработчиков до сих пор живуч миф, что JWT «безопасен сам по себе» благодаря криптографической подписи. Дескать, токен подписан, его нельзя подделать, значит можно хранить где угодно и как угодно. Это довольно опасное заблуждение. Ведь подпись защищает от подделки токена, но абсолютно бессильна против его кражи. И если злоумышленник заполучил действительный токен, он предъявит его серверу, подпись будет проверена и доступ будет предоставлен. Серверу ведь всё равно, кто именно предъявил токен, легитимный пользователь или злоумышленник использующий украденные учётные данные.

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

Стадия 1. Рождение (Issuance)

Всё начинается с момента выдачи токена. Именно на этом этапе закладывается фундамент всей последующей безопасности. И если вы допущенные ошибки на этом этапе приведут к тому, что даже самые надёжные способы хранения и передачи окажутся бесполезными.

Гранты для людей и машин

Выбор правильного OAuth-гранта определяет архитектуру безопасности с самого начала.

Для SPA и мобильных приложений сегодня существует только один рекомендуемый вариант это Authorization Code Flow с PKCE (Proof Key for Code Exchange) . Это единственный безопасный способ аутентификации для публичных клиентов, которые не могут хранить секреты. Механизм PKCE гарантирует, что даже если злоумышленник перехватит авторизационный код, он не сможет обменять его на токены без специального проверочного ключа, который генерируется на клиенте и не передаётся по сети в явном виде.

Для сервис-аккаунтов и machine-to-machine взаимодействий используется грант client_credentials. Здесь клиентское приложение аутентифицируется с помощью client_id и client_secret и получает токен без участия пользователя. Важно использовать короткоживущие токены и регулярно ротировать клиентские секреты.

А вот Implicit Flow окончательно умер. OAuth 2.1 удалил его из спецификации и использование этого гранта в 2026 году уже неприемлемо для любого аудита безопасности. Сделайте приятное своей команде - забудьте о нем.

Время жизни (TTL)

Продолжительность жизни токенов это классический и вечный компромисс между удобством пользователей и безопасностью.

Access Token оптимально должен жить не более 15 минут. Многие разработчики устанавливают 1 час «чтобы не дёргать пользователя лишний раз». Это хорошо для пользователя, но если access_token украден, злоумышленник за 1 час сможет натворить все что угодно. Тогда как при 15 минутах это окно значительно уже. Плюс, как мы увидим в блоке про отзыв, короткий TTL снижает нагрузку на системы инвалидации .

Refresh Token это более долгоживущий токен, который используется только для получения новых access-токенов. Оптимальный TTL это 24 часа или 7 дней в зависимости от чувствительности данных. Оптимальнее всего использовать «скользящую сессию» (sliding session): при каждом использовании refresh-токена срок его действия может продлеваться, но в разумных пределах. Например, при 7-дневном сроке каждое обновление может продлевать сессию ещё на 7 дней, но не более 30 дней суммарно.

Риски при выдаче и защита от брутфорса

Важный нюанс: эндпоинты /token и /login это ворота в систему, и они нуждаются в особой защите. Атака перебором клиентских секретов или учётных данных до сих пор остается нестареющей классикой, которая останется актуальной еще надолго.

Сейчас одним из лучших вариантов для этого является client_assertion - подписанный JWT, который содержит timestamp и уникальный идентификатор запроса. Сервер проверяет подпись и валидность времени, что делает бессмысленным повторное использование перехваченных запросов для брутфорса. И дополнительно не забывайте про rate limiting на уровне API-шлюза или балансировщика.

Стадия 2. Хранение на клиенте (The Big Fight)

Это самый избитый вопрос. Необходимо где то хранить токен в браузере. Вариантов насчитывается ровно три, и только один из них приемлем для применения с высокими требованиями к безопасности.

Кейс 1: localStorage / sessionStorage - почему это зло

Хранение токена в localStorage или sessionStorage это самый простой способ реализации, но и самый опасный. Любой JavaScript, выполняющийся на странице, имеет полный доступ к этим хранилищам . Это означает, что при XSS-атаке злоумышленник может выполнить localStorage.getItem('access_token') и отправить токен на свой сервер.

Несмотря на то, что все это уже достаточно давно известно и является прописными истинами проблема актуальна до сих пор. Основная причина в большинстве случаев вещает устами тимлида и звучит в большинстве случаев так:

Нужно сделать авторизацию и так чтоб при перезагрузке она не терялась. Что? Правильно через куки? А можешь сам без привлечения бэкенда сделать? Им сейчас не до тебя, они работой загружены по самые брови. Тут работы на 5 минут всего. А мы потом переделаем. Когда нибудь...

Это "когда нибудь" чаще всего не наступает никогда. А обнаружение в среднем составляет 43 дня и чаще всего уже слишком поздно.

Есть масса способов выполнения JavaScript-кода на странице, но особенно опасны атаки на цепочку поставок, когда вредоносный код внедряется в популярную NPM-библиотеку или скрипт, загружаемый с CDN. Такой код выполняется в том же контексте, что и ваше приложение, и имеет доступ ко всем данным в localStorage . Уязвимость при этом крайне плохо обнаруживается (если вообще обнаруживается) и обнаружение происходит уже в продакшене, и её исправление требует обновления зависимости, а не только вашего кода.

Кейс 2: Cookies с HttpOnly, Secure, SameSite

Использование флагов HttpOnlySecure и SameSite для хранения токена в cookie решает проблему XSS-кражи. Ключевое преимущество заключается в том что HttpOnly запрещает JavaScript доступ к содержимому cookie, поэтому даже если злоумышленник сможет внедрить скрипт, он не сможет прочитать токен. Флаг Secure гарантирует, что cookie передаётся только по HTTPS, исключая перехват на незащищённом канале. SameSite ограничивает отправку cookie только запросами с вашего сайта, защищая от CSRF.

Однако у этого подхода есть нюанс. Несмотря на выставление SameSite=Lax, существуют редкие сценарии, когда cookie будет отправлен при кросс-доменном запросе, например, при использовании POST-запросов, инициируемых с внешних сайтов. Наилучшее решение в том чтобы использовать дополнительный CSRF-токен в теле запроса или заголовке.

Серебряная пуля: BFF Pattern (Backend for Frontend)

Но если вы хотите максимальной безопасности, лучший способ вообще не отдавать токен на фронтенд .

BFF это серверная прослойка, которая размещается на том же домене, что и фронтенд, и является единственным каналом взаимодействия с внешним миром . Архитектура выглядит так:

  1. Пользователь аутентифицируется через BFF.

  2. BFF получает access и refresh токены и хранит их на сервере (в сессии или в Redis).

  3. Фронтенд получает только сессионную cookie (HttpOnly, Secure, SameSite) для идентификации текущей сессии.

  4. Все API-запросы от фронтенда идут на BFF.

  5. BFF подставляет access-токен в запросы к внешним API.

Что это даёт: токены вообще не существуют в браузере и в памяти фронтенда. Даже если произойдёт XSS-атака, злоумышленник не сможет украсть токен, так как его просто нет в контексте страницы. Кроме того, BFF защищает от проблем с блокировкой сторонних cookie, которая уже внедрена в Safari и Firefox и планируется в Chrome .

Подход с BFF явно рекомендован IETF в Best Current Practice для OAuth-приложений, работающих в браузере.

Настройка куки с флагами

В том случае если вы всё же используете cookie, настройте их максимально строго:

text

Set-Cookie: __Host-auth_token=...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=900

Особое внимание обратите на префикс __Host- он гарантирует, что cookie отправляется только с того домена, на котором был установлен, и только по HTTPS . Это защищает от «загрязнения» cookie с поддоменов.

Стадия 3. Транспорт и использование

Authorization: Bearer vs Cookie

Если вы передаёте токен в заголовке Authorization: Bearer, то это стандартный и понятный способ для API-взаимодействий. Однако есть нюанс: заголовки могут мешать кэшированию на уровне CDN. CDN обычно не кэширует ответы с заголовком Authorization, считая их приватными. Если вы кэшируете публичные данные, рассмотрите разделение API на публичные и приватные маршруты.

А при использовании cookie для токена, браузер автоматически отправляет его с каждым запросом к домену. Это упрощает работу с API, но создаёт CSRF-риски, которые мы обсуждали выше.

Передача в логах - ахиллесова пята

Но одна из самых частых утечек токенов происходит банально через логи. Access-логи Nginx, логи отладки приложения, дампы ошибок - всё это может содержать токены, если их не маскировать.

Какие при этом инструменты защиты:

  • Настройте Nginx или Envoy на маскировку заголовков Authorization и Cookie в логах.

  • Используйте структурированное логирование и не включайте полный вывод запросов в production.

  • Внедрите автоматическое сканирование логов на наличие JWT-подобных строк с отправкой оповещений в SOC.

Replay Attacks и jti

JWT содержит поле jti (JWT ID) уникальный идентификатор токена . Сохраняйте использованные jti в Redis с TTL, равным сроку жизни токена. Если токен с таким jti предъявляется повторно, это явный признак replay-атаки . Даже в рамках короткого 15-минутного TTL это важно: злоумышленник может перехватить и использовать токен несколько раз, пока он действителен.

Стадия 4. Обновление (Refresh Flow)

Обновление токенов в SPA это определенно самый сложный этап с точки зрения UX и безопасности.

Тихая фоновая ротация

Старый кондовый способ в виде использования iframe с prompt=none для тихого обновления токенов в фоне больше не работает. Safari и Firefox уже блокируют сторонние cookie, необходимые для этого механизма, Chrome вскоре планирует последовать их примеру . Все эти браузеры воспринимают prompt=none как попытку отслеживания и блокируют запрос.

Актуальный стандарт на текущий момент это Refresh Token Rotation (RT Rotation). Суть заключается вот в чем: каждый раз при использовании refresh-токена он становится недействительным, а сервер выдаёт новый refresh-токен вместе с новым access-токеном. Это ограничивает последствия кражи: даже если злоумышленник и украл refresh-токен, он сможет использовать его только один раз.

Детекция компрометации через Refresh

Ключевая фишка RT Rotation это обнаружение кражи. Если злоумышленник украл refresh-токен, но не успел его использовать, а легитимный пользователь уже обновил свою сессию, старый токен становится недействительным. Если злоумышленник попытается его использовать, сервер увидит попытку использовать уже недействительный refresh-токен, это есть явный признак компрометации.

В таких случаях сервер должен применить политику burn the family: инвалидировать все токены, принадлежащие этой семье (все refresh-токены, порождённые одной сессией), и принудительно разлогинить пользователя. Да, это создаёт дискомфорт для пользователя, но это лучше, чем позволить злоумышленнику сохранить доступ после обнаружения атаки.

Важный нюанс: реализация должна быть атомарной, чтобы избежать состояния гонки (race condition). Недавняя уязвимость CVE-2026-53517 в популярной библиотеке Better Auth как раз возникла из-за отсутствия атомарности: два одновременных запроса с одним refresh-токеном создавали вилку: две ветки одного refresh-токена, каждая из которых давала доступ . Это позволяло украденному токену сохранять неограниченный доступ. Исправление потребовало внедрения сравнения с блокировкой (compare-and-swap) в транзакцию обновления .

Пример логики работы

На фронтенде (React-хук,Vue-композиция, Angular-interceptor) обязательно нужно обрабатывать ситуацию, когда refresh-токен недействителен. Если запрос вернул 401, нельзя просто попытаться обновить токен, нужно обязательно разлогинить пользователя и перенаправить на страницу входа.

Стадия 5. Смерть и Отзыв

Главный миф о JWT гласит: «JWT нельзя отозвать до истечения срока». Это неправда. Отзыв возможен и даже необходим в ряде сценариев.

Черный список (Blocklist)

Простой но затратный способ. Если вы храните токены в чёрном списке, проблема в том, что список может бесконечно расти (и чаще всего так и делает). Наилучшее решение в этом случае использовать Redis с TTL, равным остатку жизни токена. Токен автоматически удаляется из списка, когда истекает его срок действия.

Для высоконагруженных систем эффективнее использовать Bloom Filter. Это структура данных, которая позволяет с высокой вероятностью определить, содержится ли элемент в множестве, занимая при этом минимальный объём памяти. При обнаружении компрометации добавляем jti токена в Bloom Filter. Проверка на каждом запросе O(1). Ложноположительные срабатывания возможны, но они редки и в случае сомнения можно сделать дополнительную проверку по базе данных .

Сценарии форсированного отзыва

  • Смена пароля: отзываем все активные токены пользователя. Это стандартная практика, предотвращающая сохранение доступа после компрометации пароля.

  • Подозрительный IP или User-Agent: если запрос пришёл с нового местоположения или незнакомого устройства, отзываем конкретный токен по jti и требуем повторную аутентификацию.

  • Выход пользователя (Logout): при явном выходе удаляем refresh-токен из базы данных (или помечаем как недействительный). Без этого он продолжает существовать и может быть использован даже после «выхода».

Инвалидация на уровне БД (Stateful JWT)

Можно хранить хэш токена в таблице user_sessions с полем revoked_at. Проверка при каждом запросе это обращение к БД. Плюс: мгновенный отзыв, никаких задержек, связанных с TTL. Минус: потеря stateless-преимущества и дополнительная нагрузка на БД.

Для высоконагруженных систем, особенно в финансовом и медицинском секторах, где безопасность критична, отказ от stateless в пользу более строгого контроля доступа будет оправдан. Также это решение хорошо работает в сочетании с BFF, где количество запросов на проверку токена ограничено и легко масштабируется.

Стадия 6. Мониторинг и Detective Controls

Безопасность это не про предотвращение атак, это про их быстрое обнаружение. Вот несколько аномалий, при которых нужно сразу бить тревогу:

  • Два refresh-запроса с разных IP за 1 секунду: классический признак кражи и конкурентного использования refresh-токена. Сработает триггер на инвалидацию семьи .

  • Огромный размер токена: атака на парсинг JWT через инжекцию большого количества данных в claims. Установите лимит на максимальный размер токена (например, 4KB).

  • Использование недействительного refresh-токена: явный признак того, что кто-то пытается использовать токен после его ротации. Это может быть как легитимный пользователь с устаревшей сессией, так и злоумышленник. Пользователь поворчит но перелогинится.

Ханипоты (Honeytokens)

Можно подкладывать в localStorage фейковый токен без прав. Если этот токен будет использован в API-запросах, это означает, что скрипт-злоумышленник прочитал хранилище и попытался применить данные. Немедленно оповещайте службу безопасности и разлогинивайте пользователя. Это позволяет обнаружить XSS-атаку в самом начале, до кражи настоящих данных .

Заключение

Безопасность API-токенов в 2026 году это комплексная задача, требующая внимания на всех уровнях архитектуры.

Краткое резюме:

  • Откажитесь от localStorage для чувствительных токенов в production-приложениях.

  • Хранение токенов в httpOnly куках защищает от XSS, но требует защиты от CSRF.

  • BFF-прослойка это золотой стандарт, изолирующий фронтенд от прямого доступа к токенам.

  • Refresh Token Rotation с атомарной реализацией и детекцией повторного использования уже обязательный компонент современных SPA.

  • Оперативный отзыв через Redis/Bloom Filter и мониторинг аномалий завершают картину.

  • Внедрите мониторинг аномалий: анализируйте поведение при обновлении токенов и используйте ханипоты.