Как подделать JWT, зная только публичный ключ?

Как подделать JWT, зная только публичный ключ?

Про JWT уже написали столько, что очередной разбор заголовка, данных и подписи хочется пролистнуть. Поэтому я хочу разобрать конкретную уязвимость: как пользователь может вписать себе role=admin и пройти проверку подписи.

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

Уязвимость называется algorithm confusion — подмена алгоритма. Атакующий указывает в токене другой алгоритм и подписывает его публичным ключом сервера. Уязвимый код использует этот ключ как HMAC-секрет.

Проверка сходится — и API принимает права, которые пользователь выдал себе сам.

Сначала — как должно работать

Подписанный JWT состоит из трёх частей:

заголовок.данные.подпись

В данных лежит, например, role=user. Если заменить её на role=admin, старая подпись перестанет сходиться. Сервер должен отклонить токен.

Подписывать JWT можно разными алгоритмами. Нам нужны два:

  • RS256: приватным ключом подписывают, публичным проверяют.
  • HS256: одним секретным ключом и подписывают, и проверяют.

Публичный ключ RS256 можно раскрывать. Секрет HS256 — нельзя.

Теперь посмотрим, как сервер может перепутать одно с другим.

Один удобный метод проверки

Допустим, API работает с RS256. Разработчик передаёт библиотеке токен и публичный ключ:

verify(token, publicKey)

Выглядит логично. Вот токен, вот ключ, проверь подпись.

Но в уязвимой реализации алгоритм выбирается из поля alg самого токена. Разработчик не ограничил допустимые алгоритмы, а библиотека позволяет использовать публичный RSA-ключ как секрет для HS256.

То есть сервер ещё не проверил, можно ли доверять токену, но уже выполняет его указание: «Проверяй меня через HS256».

Как из пользователя получается администратор?

Представим, что API берёт роль из JWT.

Атакующий меняет role=user на role=admin, указывает alg=HS256 и вычисляет новую подпись. В качестве секрета использует публичный ключ сервера.

Сервер получает этот токен:

  • читает alg=HS256;
  • берёт переданный разработчиком публичный ключ;
  • использует его как HMAC-секрет;
  • проверяет подпись.

Она сходится.

Атакующий и сервер использовали один и тот же ключ. Для этого байты его записи должны совпасть, включая формат и переносы строк.

Всё. Если остальные проверки пройдены, API принимает поддельный токен с ролью администратора. Приватный ключ атакующему так и не понадобился. Этот механизм описан в разборе уязвимостей JWT-библиотек у Auth0.

Что здесь нужно исправить?

Я бы начал с правила: способ проверки задаёт сервер.

API рассчитан на RS256 — принимает только RS256. Пришёл HS256 — отказ.

Тип ключа тоже проверяется. Публичный RSA-ключ нельзя передавать в HMAC как секрет. RFC 8725 требует привязывать каждый ключ к одному алгоритму.

Само наличие RS256 и HS256 в библиотеке ещё не означает уязвимость. Атака работает, когда проверяющий код допускает такую подмену и путает назначение ключей.

Поэтому я бы добавил конкретный тест: отправить в API, рассчитанный на RS256, токен HS256, подписанный публичным RSA-ключом. Ожидаемый результат — отказ.

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

Проверка подписи есть. Осталось убедиться, что поддельный токен её не проходит. Для этого и нужен тест.

Селькин Андрей
TGM / Go-разработчик из Fintech. Инженерные заметки: https://t.me/andrei_selkin_outbox
22