Безопасность JWT: типичные ошибки и как их избежать
Продвинутое4 минРедакция Innovialabs
Коротко
JWT безопасен, только если сервер жёстко задаёт алгоритм, использует сильный ключ, проверяет срок жизни, издателя и аудиторию, выдаёт короткие access-токены с ротацией refresh-токенов и хранит их вне досягаемости JavaScript.
Короткий ответ
JSON Web Token — это подписанный кусок JSON: header.payload.signature. Подпись доказывает, что токен выпустили вы и его не меняли. И только это. Payload лишь закодирован в Base64URL, не зашифрован, а валидная подпись ничего не говорит о том, вышел ли пользователь из системы или заблокирован ли он.
Большинство уязвимостей JWT — это доверие заголовку токена, слабые ключи и отсутствие управления жизненным циклом. Разберём типичные ошибки и способы их исправить.
Ошибка 1: «alg: none» и подмена алгоритма
В заголовке токена есть alg — алгоритм подписи. Если сервер берёт алгоритм из самого токена, им управляет атакующий:
- alg none — атакующий удаляет подпись и ставит
"alg": "none". Неаккуратная библиотека принимает неподписанный токен. - Algorithm confusion — сервер ждёт RS256 (подписывает приватный ключ, проверяет публичный). Атакующий меняет алгоритм на HS256 и подписывает токен вашим публичным ключом как HMAC-секретом. Если библиотека использует один и тот же ключ для обоих случаев, подделка проходит.
Решение: жёстко задайте допустимые алгоритмы на сервере и проверяйте издателя и аудиторию.
import { jwtVerify } from "jose";
const { payload } = await jwtVerify(token, publicKey, {
algorithms: ["RS256"],
issuer: "https://auth.example.com",
audience: "api",
});
Используйте поддерживаемую библиотеку и никогда не принимайте решения о доступе по токену, который только декодирован, но не проверен.
Ошибка 2: слабые секреты
При HS256 любой, у кого есть хотя бы один токен, может подбирать секрет офлайн, не трогая ваш сервер. secret, changeme или название проекта найдутся быстро.
- Используйте длинный случайный ключ, например 256 бит, сгенерированных через
openssl rand -base64 32. - Храните его в менеджере секретов или переменных окружения, а не в репозитории.
- Для разных окружений — разные ключи.
- Если токены проверяют несколько сервисов, выбирайте асимметричные алгоритмы (RS256, ES256, EdDSA): приватный ключ есть только у сервиса авторизации.
- Продумайте ротацию ключей через заголовок
kidи JWKS-эндпоинт.
Ошибка 3: чувствительные данные в payload
Payload может раскодировать кто угодно. Не кладите туда пароли, паспортные данные, полные адреса и внутренние секреты. Оставьте только то, что нужно API: ID пользователя, роли, срок жизни. Если данные нужно скрыть, используйте зашифрованные токены (JWE) или храните данные на сервере.
Ошибка 4: токены в localStorage
Всё, что лежит в localStorage, читает любой JavaScript на странице. Одна XSS-уязвимость или скомпрометированный сторонний скрипт — и токены украдены и используются с другого устройства.
Более безопасные варианты:
- Хранить токены в cookie с флагами HttpOnly, Secure, SameSite — JavaScript их не прочитает. Тогда добавьте CSRF-защиту для запросов, меняющих данные.
- Держать access-токен только в памяти и получать его заново через refresh при загрузке страницы.
- Для SPA рассмотреть паттерн Backend-for-Frontend: браузер хранит сессионную cookie, а токены — у BFF.
Ни один вариант не лечит XSS — скрипт на вашей странице всё равно может делать запросы от имени пользователя. Но простую кражу долгоживущих токенов это останавливает.
Ошибка 5: нет срока жизни и отзыва
Токен без exp действует вечно. Токен с долгим сроком нельзя отозвать после выхода, смены пароля или блокировки аккаунта.
- Всегда задавайте
expи проверяйтеexp,nbf,issиaud. - Делайте access-токены короткоживущими — минуты, а не дни.
- Для мгновенного отзыва используйте denylist по
jti, версию токенов пользователя в базе или сверяйте критичные действия с состоянием на сервере.
Ошибка 6: refresh-токены без ротации
Refresh-токен живёт долго, поэтому это самая ценная цель.
- Ротация при каждом использовании: каждое обновление выдаёт новый refresh-токен и аннулирует старый.
- Обнаружение повторного использования: если пришёл уже использованный refresh-токен, значит, у кого-то есть копия — отзовите всё семейство токенов и потребуйте вход.
- Храните refresh-токены на сервере в виде хешей с привязкой к устройству или сессии, чтобы пользователь мог видеть и завершать сеансы.
- Отправляйте refresh-токен только на эндпоинт авторизации, а не во все API.
Когда сессии лучше
JWT хорош, когда несколько независимых сервисов должны проверять личность без общей базы. Для многих продуктов классические серверные сессии проще и безопаснее:
- Одно веб-приложение на одном домене.
- Нужен мгновенный выход и кнопка «завершить все сеансы».
- Админки и внутренние инструменты.
Случайный ID сессии в HttpOnly-cookie с хранением на сервере исключает большинство описанных ошибок по самой конструкции.