Эксплуатация уязвимости JWT: algorithm confusion от бага в коде до рабочего PoC

Три критических CVE в JWT-библиотеках за первый квартал 2026 года — Hono, Parse Server, Apache Camel. Корневая причина одна и та же: парсер доверял полю alg из заголовка токена. Того самого токена, который контролирует атакующий. Hono крутится на Cloudflare Workers и Deno, Parse Server обслуживает мобильные бэкенды, Apache Camel связывает enterprise-сервисы. Ни один из этих проектов не написан джунами на коленке — и все допустили одну ошибку. Разбираем, как algorithm confusion атака работает изнутри, воспроизводим эксплойт шаг за шагом и собираем чек-лист, который AppSec-инженер (специалист по безопасности приложений) должен применять на каждом код-ревью.
Почему algorithm confusion — самая опасная уязвимость JWT-библиотеки
JWT (JSON Web Token) — компактный токен для передачи данных между сервисами. Три части через точку: header (заголовок — какой алгоритм подписи), payload (полезная нагрузка — кто пользователь, какая роль, когда истекает), signature (подпись, которая гарантирует целостность). Все три кодируются в Base64URL — вариант Base64, безопасный для использования в URL.
Подпись — единственное, что мешает атакующему подменить содержимое токена. Генерируется по алгоритму из поля alg заголовка. Два основных семейства:
- HS256 (HMAC-SHA256) — симметричный. Один секретный ключ и для подписи, и для проверки. Ключ обязан быть секретом.
- RS256 (RSA-SHA256) — асимметричный. Подпись создаётся приватным ключом, проверяется публичным. Публичный ключ не секрет — часто лежит на стандартном эндпоинте
/.well-known/jwks.json.
Algorithm confusion (она же key confusion) — ситуация, когда сервер ожидает RS256, но принимает токен с alg: HS256 и использует публичный ключ как HMAC-секрет. HMAC-функции всё равно, что переданные байты «на самом деле» RSA-ключ — для HMAC это просто входная строка. Атакующий берёт публичный ключ (он же открытый, он же доступен всем), подписывает им произвольный payload — и сервер это принимает.
Почему так получается? Согласно исследованию PortSwigger, многие JWT-библиотеки предоставляют одну универсальную функцию verify(token, secretOrPublicKey). Она сама определяет алгоритм из заголовка входящего токена. Разработчик передаёт публичный ключ, ожидая только RS256. Но атакующий присылает токен с alg: HS256 — функция обрабатывает тот же публичный ключ как HMAC-секрет, и подпись совпадает. Вот так — одна строка кода без явного параметра algorithms, и всё рассыпается.
Зачем это атакующему? Поддельный JWT с произвольными claims (полями данных) позволяет: захватить любой аккаунт, подставив чужой sub (идентификатор пользователя); эскалировать привилегии, выставив "role": "admin"; перемещаться между микросервисами, которые доверяют одному издателю токенов. В терминах MITRE ATT&CK (открытая база тактик и техник атак — по сути, каталог того, что атакующие реально делают; T-коды вроде T1190 — идентификаторы конкретных техник) цепочка выглядит так: Exploit Public-Facing Application (T1190, Initial Access) — эксплуатация уязвимого JWT-эндпоинта. Затем Valid Accounts (T1078) — атакующий получает доступ от имени легитимного пользователя. Далее Web Session Cookie (T1550.004, Lateral Movement) — поддельный токен используется для перемещения между сервисами.
Уязвимость попадает под OWASP A02:2021 — Cryptographic Failures (OWASP Top 10 — рейтинг десяти самых критичных рисков для веб-приложений; A02 — ошибки криптографии, ведущие к компрометации данных).
Разбор CVE: уязвимости парсера JWT в 2024–2026
CVE-2026-22817 — Hono: подмена алгоритма в JWK-валидации
Затронут npm-пакет hono, все версии до 4.11.4. Hono — веб-фреймворк для Cloudflare Workers, Deno, Bun и Node.js.
CVSS: 8.2 (HIGH). Вектор CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N — стандартная строка, описывающая параметры уязвимости. Если видите такую впервые, вот что значит каждый элемент:
- AV:N — атака по сети, физический доступ не нужен
- AC:L — низкая сложность эксплуатации
- PR:N — привилегии не нужны, атакует неаутентифицированный пользователь
- UI:N — действие жертвы не требуется
- I:H — высокое влияние на целостность: можно подделать токен с любыми правами
CWE-347 (CWE — классификатор типов уязвимостей; каждый номер описывает конкретный класс ошибки) — Improper Verification of Cryptographic Signature.
Суть бага: JWT-middleware Hono при валидации извлекало JWK (JSON Web Key — стандартный формат представления криптоключей) из JWKS-эндпоинта сервера. Если выбранный JWK не содержал явного поля alg, middleware брало значение alg из заголовка входящего токена. Атакующий подставлял HS256, middleware использовало публичный ключ как HMAC-секрет — поддельный токен проходил валидацию.
EPSS (оценка вероятности от 0 до 1, что уязвимость начнут эксплуатировать в ближайшие 30 дней): 0.0016, перцентиль 5.83% — ниже медианы. Решение CISA SSVC (система приоритизации уязвимостей от американского агентства кибербезопасности): Track — мониторить. Эксплуатация автоматизируема, но активных эксплойтов на момент оценки нет.
Фикс: обновление до hono >= 4.11.4. В исправленной версии параметр alg стал обязательным при настройке middleware — значение из токена больше не используется.
CVE-2026-27804 — none algorithm атака JWT и key confusion в Parse Server
Затронут npm-пакет parse-server, версии до 8.6.3 и до 9.1.1-alpha.4 (по данным NVD).
CVSS: 9.3 (CRITICAL) по вектору CVSS 4.0 — AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N. Удалённая эксплуатация без привилегий и без участия пользователя, высокое влияние на конфиденциальность и целостность.
Два CWE одновременно — каждый описывает отдельный аспект проблемы:
- CWE-327 — Use of a Broken or Risky Cryptographic Algorithm
- CWE-345 — Insufficient Verification of Data Authenticity
Суть бага: OAuth-адаптеры Parse Server для Google, Apple и Facebook извлекали alg из заголовка входящего JWT и передавали его напрямую в jwt.verify(). Два вектора атаки:
Вектор 1 — none algorithm атака JWT. Атакующий отправляет токен с alg: "none" и убирает подпись (оставляя завершающую точку: header.payload.). Сервер видит алгоритм none и пропускает проверку подписи целиком. По данным dev.to, вариации регистра (nOnE, NONE) обходили простые строковые проверки в некоторых библиотеках. Звучит глупо — но работало.
Вектор 2 (теоретический, не подтверждён advisory) — RS256 в HS256 key confusion. Теоретически атакующий мог бы поставить alg: "HS256" и подписать токен публичным ключом Google как HMAC-секретом. NVD описывает CVE-2026-27804 исключительно через none-algorithm вектор. Подтверждённый результат вектора 1 — полный захват любого аккаунта, включая администраторские.
CISA SSVC: Track, автоматизируема, технический импакт — полный (total).
Фикс: обновление до parse-server >= 8.6.3 (ветка 8.x) или >= 9.1.1-alpha.4 (ветка 9.x). В исправленном коде algorithms жёстко задан как ['RS256'] — значение из токена игнорируется.
CVE-2024-54150 — JWT signature bypass в C-библиотеке: ход мысли при код-ревью
Эту уязвимость нашёл исследователь PentesterLab при ревью C-библиотеки xmidt-org/cjwt. Ход его мысли показателен — именно так стоит смотреть на код, когда ищешь проблемы с JWT.
CVSS: 8.7 (HIGH) по CVSS 4.0. CWE-347 — та же некорректная проверка подписи.
По данным PentesterLab, три красных флага в коде:
Флаг 1. Функция cjwt_decode() принимает ключ как uint8_t * — сырые байты. Одна сигнатура вызова и для RSA, и для HMAC. Библиотека не различает тип ключа на уровне API. Уже подозрительно.
Флаг 2. Внутри cjwt_decode() вызов process_header_json() читает значение alg из заголовка входящего токена и записывает его в структуру. Алгоритм определяется данными, которые контролирует атакующий.
Флаг 3. Функция проверки подписи jws_verify_signature() содержит switch по jwt->header.alg — значению из токена. Атакующий управляет ветвлением.
Три наблюдения — и уязвимость подтверждена. Исследователь написал PoC: взял публичный ключ из примера библиотеки, подписал произвольный payload как HMAC-SHA256, и cjwt принял поддельный токен. Отчёт: GHSA-9h24-7qp5-gp82.
Связанный паттерн: CVE-2026-23552 — Apache Camel Keycloak
Отдельный класс ошибки, но последствие то же. Компонент camel-keycloak (Maven, версии 4.15.0–4.17.x) проверял подпись JWT, но не валидировал claim iss (issuer — издатель) против настроенного realm Keycloak. Токен от злоумышленника из произвольного realm принимался сервисом, настроенным на совершенно другой realm. CVSS 9.1 (CRITICAL), CWE-346 — Origin Validation Error. Фикс: обновление camel-keycloak до >= 4.18.0.
Корневая причина та же: JWT-валидация доверяет данным внутри самого токена вместо проверки против серверной конфигурации.
Algorithm confusion атака: делай раз, делай два, делай три
Пошаговая эксплуатация JWT signature bypass через подмену RS256 на HS256. Повторять только на собственном стенде или в рамках авторизованного пентеста — иначе это статья УК, а не статья блога.
Предпосылки: Python 3.8+, установленный PyJWT (pip install PyJWT), доступ к публичному ключу целевого сервиса.
Шаг 1. Получи публичный ключ сервера. Запроси стандартный эндпоинт: curl https://target.example/.well-known/jwks.json. В ответе — JSON с массивом keys, содержащим RSA-ключи в формате JWK. Сохрани ключ в файл public.pem, конвертировав из JWK в PEM (через jwt.io, openssl или Burp Suite JWT Editor). Ключ должен быть побайтово идентичен серверному — включая символы переноса строки и формат X.509 PEM.
Шаг 2. Собери payload. Разберись, какие claims использует приложение: sub (идентификатор), role, exp (срок действия). Подставь нужные значения — например, "role": "admin", "sub": "1337".
Шаг 3. Подпиши токен как HS256, используя публичный ключ как секрет.
import jwt
with open("public.pem", "r") as f:
pub_key = f.read()
print(repr(pub_key)) # проверь переносы строк: \n, не \r\n
forged = jwt.encode(
{"sub": "1337", "role": "admin"},
pub_key,
algorithm="HS256"
)
print(forged) # готовый поддельный токен
На выходе — строка из трёх Base64URL-частей. Декодируй заголовок через echo <header> | base64 -d — увидишь {"alg":"HS256","typ":"JWT"}. Если PyJWT выбросил ошибку — проверь формат ключа: PEM должен начинаться с -----BEGIN PUBLIC KEY-----. Если сервер возвращает 401 при корректном формате — сравни байты: xxd public.pem и убедись, что переносы строк (\n, не \r\n) и финальный \n совпадают с серверным ключом. Мелочь, но на ней спотыкаются чаще, чем на самой криптографии.
Шаг 4. Отправь токен на сервер. Подставь результат в заголовок Authorization: Bearer <forged_token> и отправь запрос к защищённому API. Если сервер уязвим — ответ придёт с правами из payload (200 OK с данными администратора). Если 401 или 403 — сервер корректно проверяет заданный алгоритм (algorithm pinning), атака не прошла. На бумаге формула понятна, но ощущение «щелчка» приходит только когда видишь 200 OK с чужими данными на собственном стенде. Готовый сценарий для такой практики можно собрать в HackerLab.pro — российская CTF-платформа экосистемы Codeby с категориями web, crypto и другими, нужна регистрация.
AppSec код-ревью JWT: красные флаги и автоматизация
На код-ревью задача — поймать уязвимость ДО продакшена. Вот конкретные паттерны, на которые стоит реагировать.
Флаг 1: verify() без явного списка алгоритмов. Самый частый баг. Вызов jwt.verify(token, key) без параметра algorithms — библиотека возьмёт алгоритм из заголовка токена. Безопасные вызовы по языкам:
- Node.js:
jwt.verify(token, key, { algorithms: ['RS256'] }) - Python (PyJWT >= 2.10.1):
jwt.decode(token, key, algorithms=["RS256"]) - Go (
golang-jwt/jwt):jwt.Parse(tokenString, keyFunc, jwt.WithValidMethods([]string{"RS256"}))
Версии PyJWT ниже 2.10.1 содержат известные уязвимости (GHSA-75c5-xw7c-p5pm, GHSA-752w-5fwx-jx9f). GHSA-993g-76c3-p5m4 описывает SSRF через PyJWKClient — обновляйтесь до последней патченной версии.
Флаг 2: один тип переменной для разных ключей. Если функция принимает ключ как строку или массив байтов без проверки типа — RSA и HMAC разделяют один параметр. Именно это было в cjwt: uint8_t * не различает RSA-ключ и HMAC-секрет.
Флаг 3: алгоритм из токена управляет ветвлением. Конструкция algorithm = token.header["alg"]; switch(algorithm) — прямой путь к algorithm confusion. Алгоритм должен приходить из серверной конфигурации, точка.
Для автоматизации подходит Semgrep (инструмент статического анализа кода по паттернам — по сути, «grep на стероидах» для поиска уязвимых конструкций). Концептуальное правило для Node.js:
rules:
- id: jwt-no-algorithm-pinning
pattern: jwt.verify($TOKEN, $KEY)
message: "jwt.verify без algorithms — risk of alg confusion"
severity: ERROR
Это демонстрация концепции — в реальном проекте потребуется адаптация под конкретную библиотеку. CodeQL (семантический анализатор от GitHub) позволяет отследить data flow глубже: откуда приходит значение alg и попадает ли оно в функцию верификации без проверки.
Чек-лист безопасности JWT токенов для код-ревью
| Проверка | Что искать | Последствие нарушения |
|---|---|---|
| Algorithm pinning | Параметр algorithms явно задан в конфигурации |
Algorithm confusion (CVE-2026-22817, CVE-2026-27804) |
Запрет none |
Библиотека отклоняет alg: none (включая nOnE, NONE) |
None algorithm атака — полный обход подписи |
| Разделение ключей | Симметричные и асимметричные ключи в разных переменных | Key confusion через общий параметр |
Валидация iss |
Claim iss проверяется против серверной конфигурации |
Cross-realm bypass (CVE-2026-23552) |
Валидация exp |
Срок действия проверяется при каждом запросе | Replay-атака просроченным токеном |
Запрет jku/x5u |
Сервер не загружает ключи по URL из заголовка токена | JWKS injection — подмена ключа |
| Версия библиотеки | JWT-зависимость обновлена | Известные CVE |
Минимальные безопасные версии по результатам Q1 2026:
- hono (npm): >= 4.11.4
- parse-server (npm): >= 8.6.3 (ветка 8.x) или >= 9.1.1-alpha.4 (ветка 9.x)
- camel-keycloak (Maven): >= 4.18.0
Три CVE за один квартал с одной корневой причиной — не совпадение. Это системная проблема дизайна JWT-библиотек. Спецификация RFC 7519 позволяет токену нести метаданные о способе собственной верификации, и каждая библиотека заново решает, доверять этим метаданным или нет. Большинство решают неправильно — не по злому умыслу, а потому что API библиотеки делает небезопасный вызов дефолтным, а безопасный требует дополнительного параметра. Разработчики, которые отлично понимают разницу между RSA и HMAC, всё равно допускают algorithm confusion — потому что verify(token, key) «работает» без лишних аргументов. Проблема не в знании криптографии, а в доверии к API.
Именно поэтому на код-ревью AppSec должен смотреть не на квалификацию автора PR, а на конкретные вызовы verify() и их параметры. Одна строка конфигурации — algorithms: ['RS256'] — закрывает целый класс атак. Но чтобы эту строку требовать, нужно понимать, почему без неё всё ломается. На IB Basics на codeby.school разбирают именно эту связку — от устройства уязвимостей до первых задач в AppSec.
Эту тему и смежные навыки разбирают на практике в курсе «Профессия: инженер по безопасности приложений (AppSec)» Codeby Academy.