JWT na Prática: Vulnerabilidades e Como Evitá-las
Como JWT Funciona
Um JWT tem três partes separadas por .:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJ1c2VyIn0.assinatura
header (base64) payload (base64) signatureA segurança inteira depende da assinatura. Sem ela (ou com ela mal verificada), qualquer um pode forjar tokens.
Vulnerabilidade 1: “none” Algorithm Attack
Em 2015, uma vulnerabilidade crítica foi descoberta: aceitar "alg": "none" no header permite remover a assinatura completamente.
Ataque:
// Header modificado pelo atacante:
{ "alg": "none" }
// Payload modificado:
{ "userId": 1, "role": "admin" }
// Token sem assinatura — e alguns servidores aceitavam!
Proteção:
import jwt from 'jsonwebtoken';
function verificarToken(token) {
// SEMPRE especifique os algoritmos aceitos
return jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'] // nunca adicione 'none'
});
}Vulnerabilidade 2: Algorithm Confusion (RS256 → HS256)
Se o servidor aceita RS256 (assimétrico) e HS256 (simétrico), um atacante pode mudar o alg para HS256 e assinar com a chave pública (que é pública!).
Proteção:
import jwt
PUBLIC_KEY = open('public.pem').read()
def verificar_token(token: str):
# Especifique APENAS o algoritmo esperado
return jwt.decode(
token,
PUBLIC_KEY,
algorithms=['RS256'], # não aceite HS256
)Vulnerabilidade 3: Weak Secrets
Secrets fracos podem ser quebrados por força bruta com ferramentas como hashcat ou jwt-cracker.
# Ataque com dicionário — quebra secrets fracos em segundos
hashcat -a 0 -m 16500 token.txt wordlist.txtCorreto: Use secrets de pelo menos 256 bits (32 bytes) aleatórios:
import secrets
JWT_SECRET = secrets.token_hex(32) # 64 chars hex = 256 bitsVulnerabilidade 4: JWT sem Expiração
// VULNERÁVEL — token válido para sempre
jwt.sign({ userId: 1 }, secret)
// CORRETO — expira em 15 minutos
jwt.sign({ userId: 1 }, secret, { expiresIn: '15m' })Implemente refresh tokens para sessões longas:
// Access token curto (15min)
const accessToken = jwt.sign({ userId }, ACCESS_SECRET, { expiresIn: '15m' });
// Refresh token longo, armazenado no banco e revogável
const refreshToken = jwt.sign({ userId, jti: uuid() }, REFRESH_SECRET, { expiresIn: '7d' });Vulnerabilidade 5: Dados Sensíveis no Payload
O payload JWT é apenas Base64 — não é criptografado. Qualquer pessoa com o token pode ler o conteúdo.
// NUNCA coloque isso no payload:
{
userId: 1,
senha: "minhasenha", // ❌
cpf: "123.456.789-00", // ❌
cartaoCredito: "4111..." // ❌
}
// OK no payload:
{
userId: 1,
role: "user",
exp: 1718000000
}Onde Armazenar o JWT?
| Local | XSS | CSRF | Recomendado |
|---|---|---|---|
| localStorage | Vulnerável | Seguro | Não |
| Cookie (httpOnly) | Seguro | Vulnerável | Sim + CSRF token |
| Cookie (httpOnly + SameSite=Strict) | Seguro | Seguro | Sim |
| Memória JS | Seguro | Seguro | Sim (perde no refresh) |
// Cookie seguro para access token
res.cookie('access_token', token, {
httpOnly: true,
secure: true, // apenas HTTPS
sameSite: 'strict', // proteção CSRF
maxAge: 15 * 60 * 1000,
});Revogação de Tokens
JWT é stateless — por isso, revogar antes do prazo requer uma blocklist:
import redis
r = redis.Redis()
def revogar_token(jti: str, ttl_segundos: int):
r.setex(f"revogado:{jti}", ttl_segundos, "1")
def token_revogado(jti: str) -> bool:
return r.exists(f"revogado:{jti}") > 0Conclusão
JWT é seguro quando usado corretamente. Os erros mais comuns são facilmente evitáveis: especifique o algoritmo, use secrets fortes, defina expiração e não exponha dados sensíveis no payload.