Перейти к содержанию
Innovialabs

Безопасность JWT: типичные ошибки и как их избежать

Продвинутое4 мин

Коротко

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 с хранением на сервере исключает большинство описанных ошибок по самой конструкции.

Частые вопросы

Обычный подписанный JWT (JWS) — нет. Его payload может прочитать кто угодно. Для шифрования нужен JWE, который применяется гораздо реже и усложняет систему.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.