В JWT хранят отдел, должность и почту сотрудника, полагая, что токен зашифрован. Но это не так
Разработчик прикрепил к задаче полный JWT, чтобы команда могла воспроизвести ошибку авторизации. Проблему исправили, тикет закрыли, а токен остался в системе, доступной сотрудникам и подрядчикам. Если срок его действия ещё не истёк, такой случай может превратить обычную отладку в инцидент безопасности.
Главная причина подобных ошибок - неправильное представление о природе JWT. Длинная строка из символов выглядит как шифротекст, но в большинстве случаев токен не зашифрован. Его содержимое лишь закодировано в формате Base64URL. Для чтения не нужны ни секретный ключ, ни пароль.
Из чего состоит JWT
Стандартный JSON Web Token включает три части, разделённые точками:
1. заголовок;
2. полезную нагрузку;
3. криптографическую подпись.
В заголовке обычно указаны тип токена и алгоритм подписи. В payload находятся claims - утверждения о пользователе, сессии или назначении токена. Третья часть подтверждает, что первые две не были изменены после выпуска.
Если декодировать среднюю часть, можно увидеть идентификатор сотрудника, адрес электронной почты, должность, отдел, срок действия и другие поля. Никакого секрета для этого не требуется. Эти сведения доступны браузеру, установленным в нём расширениям, прокси, системам мониторинга и логам серверов, через которые проходит запрос.
Поэтому JWT нельзя рассматривать как безопасное место для хранения персональных или служебных данных. Кодирование меняет представление информации, но не скрывает её.
Что на самом деле гарантирует подпись
Подпись обеспечивает целостность и подлинность токена. Сервер вычисляет её на основе заголовка и payload, используя секретный ключ. Если злоумышленник изменит роль `manager` на `admin`, но не сможет сформировать новую корректную подпись, сервер отклонит токен.
При этом изменённое содержимое всё равно будет читаться. То есть подпись не делает данные секретными: она лишь позволяет проверить, что их не подменили.
Практическое правило выглядит так:
- прочитать payload может любой, кто получил токен;
- изменить его без ключа нельзя;
- получить сам токен уже достаточно опасно, поскольку его можно использовать до окончания срока действия.
Ошибка первая: в токен помещают слишком много данных
Часто JWT превращают в копию пользовательского профиля. В него добавляют телефон, полное имя, внутренние идентификаторы, настройки доступа и сведения о структуре организации. Затем эти данные попадают в историю запросов, логи API-шлюзов, системы трассировки и резервные копии.
Для авторизации обычно достаточно минимального набора:
- идентификатора пользователя;
- срока действия;
- издателя токена;
- аудитории;
- роли или набора разрешений, если это действительно необходимо.
Остальную информацию сервис может получить из собственной базы по идентификатору пользователя. Чем меньше данных в токене, тем ниже ущерб при его утечке и тем проще соблюдать требования к обработке персональной информации.
Ошибка вторая: токен живёт слишком долго
JWT часто называют stateless-механизмом, поскольку серверу не обязательно хранить состояние каждой сессии. Он проверяет подпись, срок действия и принимает решение на основании содержимого токена. Но у этого подхода есть важное ограничение: уже выданный токен обычно нельзя мгновенно отменить.
Если сотрудника уволили или лишили прав, его старый access token может продолжить работать до истечения срока. Когда токен действует сутки, увольнение фактически не прекращает доступ на протяжении следующих 24 часов.
Более безопасная схема предполагает:
- короткий срок жизни access token - обычно минуты;
- отдельный refresh token;
- хранение и контроль refresh token на серверной стороне;
- возможность отозвать сессию или все токены пользователя;
- повторную проверку критичных прав при выполнении особо опасных операций.
Продолжительность жизни токена стоит проверить отдельно. Такой параметр часто задают на старте проекта и годами не пересматривают.
Ошибка третья: проверяют только подпись
Наличие корректной подписи ещё не означает, что токен предназначен именно для конкретного сервиса. Библиотека может автоматически проверять `exp`, но такие поля, как `iss` и `aud`, нередко нужно явно указать в настройках.
`iss` определяет издателя токена, а `aud` - сервис или группу сервисов, для которых он выпущен. Если проверять только подпись, API биллинга может принять токен, предназначенный для административной панели. Особенно опасна такая конфигурация, когда несколько сервисов используют один ключ и доверяют одному серверу аутентификации.
Проверять необходимо как минимум:
- срок действия `exp`;
- время начала действия `nbf`, если оно используется;
- издателя `iss`;
- аудиторию `aud`;
- ожидаемый тип токена;
- обязательные claims;
- допустимый алгоритм подписи.
Нельзя доверять алгоритму из заголовка
Поле `alg` находится внутри самого токена, а значит, его контролирует отправитель. Если приложение автоматически выбирает способ проверки на основании этого поля, атакующий получает возможность влиять на процедуру валидации.
Исторически подобные ошибки приводили к принятию токенов с алгоритмом `none` и к подмене асимметричного алгоритма симметричным. В последнем случае публичный ключ мог ошибочно использоваться как секретный.
Алгоритм должен задаваться конфигурацией приложения, а не приниматься из JWT. В коде следует явно разрешать конкретный вариант - например, только нужную схему RSA или только утверждённую схему HMAC. Если список алгоритмов формируется из заголовка токена, конфигурацию необходимо исправить.
Где хранить JWT на клиенте
Выбор между `localStorage` и cookie связан с двумя основными рисками: XSS и CSRF.
Токен в `localStorage` доступен любому JavaScript-коду на странице. Если в приложении появится XSS-уязвимость или будет скомпрометирована сторонняя библиотека, скрипт сможет прочитать токен и отправить его злоумышленнику.
Cookie с флагом `HttpOnly` недоступна для JavaScript. Дополнительные параметры `Secure` и `SameSite` снижают риск передачи cookie по незащищённому соединению и межсайтовых запросов. Однако cookie автоматически прикладывается браузером к запросам, поэтому необходимо защищаться от CSRF: применять подходящий режим `SameSite`, CSRF-токены и проверку источника запроса.
Универсального варианта для всех приложений нет. Важнее понимать модель угроз и не считать одно хранилище автоматически безопасным.
Что проверить в проекте
Аудит JWT стоит начать с нескольких практических вопросов:
- Можно ли декодировать токен без ключа? Если да, какие данные там находятся?
- Не попадают ли JWT в журналы доступа, трассировки, сообщения об ошибках и тикеты?
- Какой максимальный срок действия access token?
- Есть ли механизм отзыва refresh token?
- Проверяются ли `iss`, `aud`, `exp` и `nbf`?
- Зафиксирован ли список допустимых алгоритмов?
- Не принимаются ли токены с неподходящим типом или назначением?
- Где хранится токен в браузере?
- Настроены ли `HttpOnly`, `Secure` и `SameSite`?
- Есть ли маскирование токенов в логах и системах наблюдаемости?
Как безопасно работать с токенами при отладке
Никогда не стоит прикладывать в задачу действующий JWT целиком. Для воспроизведения ошибки лучше использовать искусственный токен с тестовыми данными и коротким сроком действия. Если нужен реальный сценарий, токен следует предварительно отозвать, заменить или выпустить специально для стенда.
В логах рекомендуется автоматически маскировать заголовок `Authorization`, cookie и параметры, содержащие токены. Недостаточно скрывать только пароль: JWT также является bearer-учётными данными, то есть тот, кто получил строку, может предъявить её серверу.
Особенно важно контролировать системы, где данные живут дольше самого приложения: агрегаторы логов, резервные копии, трекеры задач, чаты разработчиков и системы аналитики. Удаление токена из интерфейса тикета не гарантирует его исчезновения из истории или резервной копии.
JWT удобен для передачи подтверждённых утверждений между сервисами, но он не является зашифрованным контейнером. В него следует помещать минимум необходимой информации, ограничивать срок действия, строго проверять назначение и алгоритм, а любой опубликованный токен считать потенциально скомпрометированным.
