Boas práticas de segurança JWT: ataques, defesas e um checklist para 2026
Os JSON Web Tokens sustentam boa parte da autenticação moderna, mas as boas práticas de segurança JWT que os mantêm protegidos são ignoradas com muito mais frequência do que deveriam. JWT é o formato de credencial de fato para OAuth 2.0, OpenID Connect e chamadas serviço a serviço dentro de microsserviços. Também é a origem de um fluxo constante de CVEs todos os anos, e quase todos remetem aos mesmos erros evitáveis: aceitar tokens não assinados, confiar no algoritmo que um atacante escolheu, usar um segredo de assinatura fraco ou pular a validação de claims.
Um JWT é seguro quando quatro condições valem ao mesmo tempo. A assinatura está intacta, o algoritmo não pode ser trocado por um atacante, os claims são de fato verificados e o token fica guardado em um lugar de onde não pode ser roubado trivialmente. Quebre qualquer uma delas e você tem um bypass de autenticação, não uma API endurecida. Este guia percorre os três ataques que mais importam e, depois, as defesas: escolher e fixar um algoritmo, gerenciar chaves, validar claims e armazenar tokens. Termina com um checklist que você pode colar direto numa revisão.
Como a assinatura de um JWT realmente protege você (e o que ela não protege)
Antes que qualquer ataque faça sentido, você precisa de um fato bem claro: um JWT é codificado, não criptografado. Um token assinado tem três segmentos Base64URL unidos por pontos: header.payload.signature. O cabeçalho e o payload são simplesmente o JSON em Base64URL. Quem tiver o token consegue ler todos os claims dentro dele. Cole qualquer token no nosso decodificador JWT e você verá o cabeçalho e o payload exibidos em JSON legível, sem precisar de nenhuma chave. O payload é público por design.
Então de onde vem a segurança? Da assinatura, e só dela. É um valor criptográfico calculado sobre o cabeçalho e o payload usando um segredo (HMAC) ou uma chave privada (RSA, ECDSA). Um atacante pode ler um token livremente, mas não consegue produzir um token diferente que passe na verificação sem a chave de assinatura. Essa única propriedade é todo o modelo de confiança.
Daí decorrem duas consequências. Primeira: nunca coloque segredos no payload (senhas, chaves de API, PII completa), porque ele é legível para qualquer um que intercepte o token. Segunda: toda a sua postura de segurança se apoia em um único passo, verificar a assinatura corretamente. É exatamente esse o passo que os atacantes atacam. Se você quiser um passo a passo mais detalhado de como ler tokens segmento por segmento, veja como decodificar um JWT.
Os 3 ataques JWT críticos (e como deter cada um)
A maioria das vulnerabilidades de JWT são variações de um mesmo tema: o servidor confia em algo que o atacante controla. Aqui estão os três que rompem a autenticação por completo, com o mecanismo por trás de cada um e a correção.
1. O ataque alg:none — bypass por token não assinado
A especificação JWS inclui um valor de alg igual a none, que significa “não assinado”. Um token alg:none tem o segmento de assinatura vazio e ainda termina com um ponto final, como header.payload.. O ataque é simples: pegue um token válido, mude o alg do cabeçalho para none, troque os claims pelos que quiser (digamos "role": "admin") e descarte a assinatura. As primeiras bibliotecas JWT aceitavam isso por padrão, então o token forjado passava reto pela verificação. Sem chave, sem assinatura, personificação completa.
Você pode ver como é um token desse tipo carregando o exemplo “alg:none” no nosso decodificador JWT. Ele exibe um alerta vermelho explícito de que o token não está assinado e nunca deve ser aceito para autenticação. Reproduzir um por conta própria é um exercício de um minuto para entender a ameaça.
A defesa é uma allowlist explícita de algoritmos em toda chamada de verificação. Nunca deixe o padrão da biblioteca decidir o que é aceitável, porque os padrões antigos eram permissivos e o custo de ser explícito é uma opção a mais.
// WRONG — the library may accept alg:none or any algorithm
jwt.verify(token, key);
// RIGHT — pin the exact algorithm you expect
jwt.verify(token, key, { algorithms: ['RS256'] });
none nunca deve aparecer nesse array. Se a sua biblioteca não consegue fixar algoritmos, troque de biblioteca.
2. Confusão de algoritmos — RS256 rebaixado para HS256
Esta é a vulnerabilidade de JWT mais perigosa na prática, conhecida desde 2015 e ainda encontrada em auditorias hoje. Ela explora servidores que decidem como verificar com base no campo alg do cabeçalho, a única parte do token que um atacante pode reescrever.
Eis o mecanismo. Seu servidor emite tokens RS256: ele assina com uma chave privada RSA e verifica com a chave pública correspondente. Essa chave pública é, por definição, pública: pode estar no seu endpoint JWKS ou no seu repositório. O atacante a pega, muda o cabeçalho do token de RS256 para HS256 e assina um payload forjado usando HMAC-SHA256 com a string da chave pública como segredo HMAC. Agora, do lado da verificação: se o seu código lê o alg do cabeçalho e escolhe HMAC de acordo, ele calcula HMAC-SHA256 sobre o token usando essa mesma chave pública como segredo. As assinaturas batem. O token forjado é aceito.
A causa raiz é a colisão de dois fatos: o verificador confiou no cabeçalho alg controlado pelo atacante, e a chave pública RSA estava disponível para o atacante usar como chave HMAC. Nenhum desses fatos é um bug por si só. Uma chave pública deve ser pública, e um cabeçalho alg deve descrever o token. A vulnerabilidade nasce quando a sua lógica de verificação deixa esse cabeçalho escolher qual tipo de chave e algoritmo usar, porque então um valor que o atacante escreve passa a comandar o caminho criptográfico que o servidor executa.
// WRONG — verification method follows the header's alg field
jwt.verify(token, publicKeyOrSecret);
// RIGHT — hard-code the expected algorithm; never let the header choose
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
Fixe o algoritmo assimétrico de forma explícita (apenas RS256 ou ES256), mantenha a verificação HMAC em um caminho de código totalmente separado da verificação RSA e use uma biblioteca mantida que distinga os tipos de chave. Nosso decodificador JWT sinaliza qualquer token da família HS com um alerta de confusão de chave pública justamente porque esse ataque é muito comum. Quando um token que você esperava ser assimétrico aparece como HS256, esse alerta é o seu sinal.
3. Segredo HMAC fraco — ataques de força bruta e dicionário
Quando você de fato usa HMAC (HS256/384/512), toda a segurança do token repousa sobre a entropia de um único segredo. Se esse segredo for curto, uma palavra de dicionário ou um valor como secret ou password123, um atacante que capture um único token válido consegue quebrá-lo offline. Ferramentas como o hashcat percorrem bilhões de candidatos por segundo contra a assinatura do token. Uma vez que o segredo cai, o atacante pode cunhar qualquer token que quiser, com credenciais admin válidas para sempre.
O que torna esse ataque silenciosamente perigoso é que ele é totalmente offline. O atacante não martela o seu endpoint de login, então não há rate limit para disparar e nada nos seus logs para perceber. Ele captura um token, quebra o segredo no próprio hardware e só volta quando já consegue assinar tokens que passam por todas as verificações que você tem. A correção é inegociável: use pelo menos 32 bytes aleatórios (256 bits) de uma fonte criptograficamente segura e guarde-o em um gerenciador de segredos, nunca no código ou num repositório.
// WRONG — guessable, low entropy, crackable in seconds
const secret = "password123";
// RIGHT — 256 bits from a CSPRNG, then load from KMS at runtime
const secret = require('crypto').randomBytes(32).toString('base64');
Precisa de um valor forte rápido? Nosso gerador de senhas produz strings de alta entropia adequadas para uma chave HMAC. Quer sentir a diferença na prática? Assine um token de teste com um segredo forte no nosso codificador JWT, que roda inteiramente no navegador, então o segredo nunca sai da sua máquina. E quando a verificação cruza uma fronteira de confiança, com vários serviços ou verificadores de terceiros, pare de usar HS256 de vez e passe para um algoritmo assimétrico, tema da próxima seção.
Escolhendo e fixando o algoritmo certo
A escolha do algoritmo é onde o ataque de confusão é ganho ou perdido, então escolha com intenção. Os três que você vai realmente usar:
| Algoritmo | Tipo | Chave de assinatura / verificação | Quando usar |
|---|---|---|---|
| HS256 | Simétrico (HMAC) | Um segredo compartilhado | Fronteira de confiança única, mesma parte assina e verifica |
| RS256 | Assimétrico (RSA) | Chave privada assina / chave pública verifica | Entre serviços, verificação por terceiros, rotação via JWKS |
| ES256 | Assimétrico (ECDSA) | Chave privada assina / chave pública verifica | Mesmo que RS256, com chaves menores e mais rápidas; preferido para sistemas novos |
A regra é curta. Se a mesma parte assina e verifica dentro de uma só fronteira de confiança, HS256 é adequado e rápido. Se alguém além de quem assina precisar verificar (outro serviço, um parceiro, um cliente público), use um algoritmo assimétrico, e prefira ES256: suas chaves e assinaturas são muito menores que as do RSA com força equivalente. Você pode assinar tokens de exemplo HS256, RS256 e ES256 lado a lado no codificador JWT para comparar a estrutura e o tamanho da assinatura.
Seja qual for a sua escolha, a defesa que de fato importa é a mesma: fixe um conjunto explícito de algoritmos na chamada de verificação e nunca confie no campo alg do cabeçalho. A allowlist é a base sobre a qual todo o resto se apoia.
Gerenciamento e rotação de chaves
Os algoritmos só são tão seguros quanto as chaves por trás deles, e o manuseio de chaves é onde a maioria dos guias se cala. Para HS256, o segredo tem pelo menos 32 bytes aleatórios e vive em um gerenciador de segredos como AWS Secrets Manager, HashiCorp Vault ou Azure Key Vault. Para algoritmos assimétricos, a chave privada pertence a um HSM ou KMS e nunca toca o código da aplicação; a chave pública é publicada, normalmente por um endpoint JWKS que os verificadores buscam.
A rotação precisa ser rotina, não emergência. Marque cada chave com um kid (key ID) no cabeçalho do JWT para que os verificadores saibam qual chave assinou um dado token. Mantenha um pequeno conjunto de chaves válidas no lado da verificação, a chave atual mais a anterior recente, para que tokens assinados pouco antes de uma rotação ainda verifiquem durante o seu tempo de vida. Essa sobreposição é o que torna a rotação contínua em vez de uma indisponibilidade.
Um checklist curto para chaves:
- Rotacione as chaves de assinatura pelo menos a cada 90 dias e imediatamente diante de qualquer suspeita de comprometimento.
- Publique as chaves públicas via JWKS; versione-as com
kid. - Mantenha as chaves privadas e os segredos HMAC em um KMS ou HSM, nunca no git, nunca no código do cliente, nunca hard-coded.
- Em caso de vazamento, rotacione a chave e revogue os refresh tokens em circulação de uma vez.
Validação de claims que você não pode pular
Verificar a assinatura prova que um token é autêntico. Não prova que o token é para você, agora. Esse é o trabalho da validação de claims, e é a defesa mais barata que você pode adicionar. Cinco claims precisam de verificação em toda requisição:
exp(expiration): rejeite tokens cuja expiração esteja no passado.nbf(not before): rejeite tokens usados antes de a janela de validade abrir.iat(issued at): opcionalmente rejeite tokens implausivelmente antigos.iss(issuer): confirme que o token veio do emissor em que você confia.aud(audience): confirme que o token foi cunhado para o seu serviço. A ausência da verificação deaudé o furo silencioso mais comum, permitindo que um token emitido para uma API seja reapresentado contra outra.
A maioria das bibliotecas valida esses claims para você quando você passa os valores esperados:
jwt.verify(token, key, {
algorithms: ['ES256'],
issuer: 'https://auth.example.com',
audience: 'api.example.com',
clockTolerance: 5, // seconds, for distributed clock skew
});
Permita uma pequena tolerância de relógio (cinco segundos é o típico) para que pequenas diferenças entre servidores não rejeitem tokens que de outra forma seriam válidos. Resista à tentação de aumentá-la. Uma tolerância generosa amplia a janela em que um token expirado ainda funciona, que é exatamente o que o exp existe para fechar. Para conferir a olho os valores de exp e iat de um token, jogue-o no decodificador JWT e converta os timestamps com o nosso conversor de timestamp Unix.
Tempo de vida do token e onde armazenar JWTs
As verificações no servidor são só metade da história. Onde o cliente guarda o token decide com que facilidade ele pode ser roubado, e o armazenamento é onde XSS e sequestro de sessão se encontram. O padrão que se sustenta: um access token de vida curta (15 a 60 minutos) combinado com um refresh token separado, de vida mais longa e revogável.
A decisão de armazenamento se resume a um único trade-off:
| Local de armazenamento | Exposição a XSS | Risco de CSRF | Recomendação |
|---|---|---|---|
| localStorage | Alta, qualquer JavaScript na página consegue ler | Nenhum | Evite para tokens de sessão |
| Cookie HttpOnly + Secure + SameSite=Strict | Baixa, invisível ao JavaScript | Precisa de proteção CSRF | Recomendado para sessões |
Um token no localStorage é legível por qualquer script que rode na página, então um único bug de XSS vaza a sessão inteira, e o atacante pode reapresentá-la a partir da própria máquina por todo o tempo de vida dela. Um cookie HttpOnly não pode ser lido pelo JavaScript de jeito nenhum, o que reduz o dano de um XSS ao que o atacante consegue fazer dentro de uma página viva. Ruim, mas não uma credencial roubada que ele pode levar embora. O custo da abordagem com cookie é que agora você precisa de proteção CSRF, já que cookies vão junto automaticamente em toda requisição; SameSite=Strict mais um token CSRF resolvem isso. Mantenha o access token curto para que um vazamento tenha um raio de impacto pequeno, e coloque o refresh token em um cookie HttpOnly, Secure, SameSite. No logout ou diante de suspeita de comprometimento, revogue o refresh token no servidor e rotacione a chave de assinatura. Para o contexto mais amplo em torno de XSS, CSRF e cookies seguros, veja o nosso guia de boas práticas de segurança web.
Checklist de segurança JWT
Passe por isto antes de subir qualquer autenticação baseada em JWT:
- A verificação fixa uma allowlist explícita de algoritmos e rejeita
alg:none. - A verificação assimétrica hard-coda o algoritmo esperado e nunca lê o
algdo cabeçalho (bloqueia a confusão). - Os segredos HS256 têm pelo menos 32 bytes aleatórios, carregados de um KMS.
- As chaves privadas vivem em um HSM/KMS; as chaves públicas são publicadas via JWKS e versionadas com
kid. - As chaves de assinatura rotacionam pelo menos a cada 90 dias.
- Toda requisição valida
exp,nbf,iat,isseaud, com uma tolerância de relógio de 5 segundos ou menos. - Os access tokens duram de 15 a 60 minutos; os refresh tokens vivem em um cookie
HttpOnly. - Nenhum segredo no payload — ele é codificado, não criptografado.
FAQ
O JWT é seguro por padrão?
Não. A segurança do JWT depende da configuração. Você precisa fixar o algoritmo, rejeitar alg:none, usar um segredo ou chave de alta entropia e validar os claims. Configurações de biblioteca padrão ou permissivas frequentemente permitem bypasses de autenticação.
Qual é a vulnerabilidade de JWT mais perigosa?
A confusão de algoritmos, em que RS256 é rebaixado para HS256 e a chave pública é usada como segredo HMAC. É conhecida desde 2015, mas ainda aparece em auditorias, porque explora servidores que escolhem o método de verificação a partir do alg do cabeçalho.
Devo usar HS256 ou RS256?
Use HS256 quando a mesma parte assina e verifica dentro de uma só fronteira de confiança. Use RS256 ou ES256 quando outro serviço ou um terceiro precisar verificar, ou quando você precisar de rotação via JWKS. Para sistemas novos, prefira ES256: chaves menores e mais rápidas com força equivalente.
Onde devo armazenar um JWT?
Prefira um cookie HttpOnly, Secure, SameSite para tokens de sessão, já que o JavaScript não consegue lê-lo e um único bug de XSS não consegue roubá-lo. Evite o localStorage para tokens de sessão — qualquer XSS vaza a sessão inteira para reapresentação.
Com que frequência devo rotacionar as chaves de assinatura JWT?
Rotacione pelo menos a cada 90 dias como rotina e imediatamente diante de qualquer suspeita de comprometimento. Versione as chaves com kid e mantenha tanto a chave ativa quanto a anterior recente no verificador para que tokens assinados pouco antes da rotação ainda validem.
Um JWT pode ser adulterado?
Não sem a chave de assinatura. Nenhum atacante consegue forjar um token que passe na verificação. Mas se o seu servidor aceita alg:none, é vulnerável à confusão de algoritmos ou usa um segredo fraco, a assinatura pode ser contornada. Essas são falhas de configuração, não falhas do JWT em si.
Quais claims eu preciso validar?
Valide exp (expiration), nbf (not before), iat (issued at), iss (issuer) e aud (audience). A ausência da verificação de aud é a vulnerabilidade silenciosa mais comum, permitindo que um token destinado a um serviço seja reapresentado contra outro.
Conclusão
A segurança do JWT não é complicada, mas cada camada precisa se sustentar. A assinatura é a sua única garantia, então verifique-a corretamente. Fixe um algoritmo explícito e nunca confie no alg do cabeçalho. Use chaves fortes, rotacionadas e mantidas em um KMS. Valide exp, nbf, iat, iss e aud em toda requisição. Armazene os tokens onde o XSS não alcance.
Para colocar isso em prática, cole qualquer token no nosso decodificador JWT para inspecionar o algoritmo e os claims e detectar riscos de alg:none ou de confusão HS, e use o codificador JWT para experimentar a assinatura inteiramente no seu navegador, onde suas chaves nunca saem do seu dispositivo.