Saltar para o conteúdo
JWT na Prática: Vulnerabilidades e Como Evitá-las
Segurança

JWT na Prática: Vulnerabilidades e Como Evitá-las

15 de maio de 2026·Paulo Pereira

Como JWT Funciona

Um JWT tem três partes separadas por .:

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJ1c2VyIn0.assinatura
    header (base64)         payload (base64)              signature

A 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.txt

Correto: Use secrets de pelo menos 256 bits (32 bytes) aleatórios:

import secrets
JWT_SECRET = secrets.token_hex(32)  # 64 chars hex = 256 bits

Vulnerabilidade 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?

LocalXSSCSRFRecomendado
localStorageVulnerávelSeguroNão
Cookie (httpOnly)SeguroVulnerávelSim + CSRF token
Cookie (httpOnly + SameSite=Strict)SeguroSeguroSim
Memória JSSeguroSeguroSim (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}") > 0

Conclusã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.