Как подделать JWT, зная только публичный ключ?
Про JWT уже написали столько, что очередной разбор заголовка, данных и подписи хочется пролистнуть. Поэтому я хочу разобрать конкретную уязвимость: как пользователь может вписать себе role=admin и пройти проверку подписи.
Без приватного ключа. Ему хватит публичного — того самого, который можно свободно раздавать.
Уязвимость называется algorithm confusion — подмена алгоритма. Атакующий указывает в токене другой алгоритм и подписывает его публичным ключом сервера. Уязвимый код использует этот ключ как HMAC-секрет.
Проверка сходится — и API принимает права, которые пользователь выдал себе сам.
Сначала — как должно работать
Подписанный JWT состоит из трёх частей:
заголовок.данные.подпись
В данных лежит, например, role=user. Если заменить её на role=admin, старая подпись перестанет сходиться. Сервер должен отклонить токен.
Подписывать JWT можно разными алгоритмами. Нам нужны два:
- RS256: приватным ключом подписывают, публичным проверяют.
- HS256: одним секретным ключом и подписывают, и проверяют.
Публичный ключ RS256 можно раскрывать. Секрет HS256 — нельзя.
Теперь посмотрим, как сервер может перепутать одно с другим.
Один удобный метод проверки
Допустим, API работает с RS256. Разработчик передаёт библиотеке токен и публичный ключ:
Выглядит логично. Вот токен, вот ключ, проверь подпись.
Но в уязвимой реализации алгоритм выбирается из поля 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-ключом. Ожидаемый результат — отказ.
На ревью я бы посмотрел, откуда берётся алгоритм проверки. Если из самого токена и без ограничений со стороны сервера — это повод задержаться в этом месте кода.
Проверка подписи есть. Осталось убедиться, что поддельный токен её не проходит. Для этого и нужен тест.