Fastway
Voltar ao blog

O que tem dentro de um JWT — e o que você nunca deve colocar lá

Rodrigo Krohling
·7 min de leitura

Pegue qualquer JWT que a sua aplicação emite e cole num decodificador. O payload aparece na hora. Sem senha, sem chave, sem requisição ao seu servidor.

Isso não é fraqueza do decodificador. É o formato funcionando como projetado — e quase todo incidente com JWT nasce de alguém supor o contrário.

Três partes, dois pontos

Um JWT é uma única string com dois pontos no meio:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0Iiwicm9sZSI6ImFkbWluIn0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

Separe nos pontos e você tem o cabeçalho, o payload e a assinatura. Os dois primeiros são JSON codificado em Base64url — reversível por qualquer um, sem chave nenhuma. Decodifique o primeiro:

{ "alg": "HS256", "typ": "JWT" }

E o segundo:

{ "sub": "1234", "role": "admin" }

A terceira parte é a assinatura: as duas primeiras partes, unidas por um ponto, passadas pelo algoritmo declarado no cabeçalho usando um segredo que só o servidor tem.

Ou seja: a garantia que um JWT oferece é integridade e autenticidade, não confidencialidade. Ele prova que o payload não foi alterado desde que o servidor assinou. Não faz absolutamente nada para esconder o payload.

Base64url, não Base64

A codificação é a variante segura para URL: + vira -, / vira _, e o padding = do fim é removido. Isso importa quando você está depurando à mão, porque colar um segmento de JWT num decodificador Base64 padrão pode falhar até você recolocar o padding.

A regra: acrescente = até o tamanho ser múltiplo de quatro. Um segmento de 22 caracteres precisa de dois, de 23 precisa de um, de 24 não precisa.

O que nunca pertence ao payload

Como qualquer um com o token consegue ler todas as claims, a lista é curta e absoluta.

Qualquer coisa que você não imprimiria num log. Senhas, obviamente. Mas também chaves de API, IDs internos de banco que você prefere não expor, números de licença e documentos. Se seria um problema num print de ticket de suporte, é um problema aqui.

Dados pessoais que você não precisa a cada requisição. E-mail e nome completo são os infratores de rotina. Eles acabam no storage do navegador, no log de acesso do servidor, no payload de relatórios de erro e em qualquer proxy que logue cabeçalhos. Sob a LGPD e o GDPR isso é tratamento que você agora precisa justificar, por uma conveniência de que provavelmente não precisava.

Qualquer coisa que muda mais rápido que o token. Um JWT é uma fotografia tirada no login. Se você põe um conjunto de permissões nele e o usuário é rebaixado, o token continua afirmando a permissão antiga até expirar. Essa é a troca fundamental dos tokens sem estado: você evita uma consulta ao banco por requisição e aceita uma janela de desatualização. Projete a expiração em torno de quanto essa janela pode durar.

As quatro claims que valem conhecer

A especificação registra várias claims. Estas são as que carregam peso:

  • exp — expiração. Segundos desde a epoch Unix.
  • iat — emitido em. Mesma unidade.
  • nbf — não antes de. O token é inválido até esse instante.
  • sub — o sujeito, ou seja, sobre quem é o token.

Essas unidades são a origem de um bug que se esconde por completo em teste. O Date.now() do JavaScript devolve milissegundos. As claims estão em segundos. Compare direto e todo token parece válido até por volta do ano 55000:

// Errado — compara milissegundos com segundos.
if (payload.exp > Date.now()) { /* sempre verdadeiro */ }

// Certo.
if (payload.exp > Math.floor(Date.now() / 1000)) { /* ... */ }

Nada falha em desenvolvimento, porque os tokens ainda não expiraram. Aparece em produção na primeira vez que um token expirado é aceito. Se você está olhando um timestamp e quer saber o que ele significa, um conversor de timestamp é mais rápido que fazer a conta.

O problema do alg: none

O cabeçalho declara qual algoritmo assinou o token. Um verificador que lê esse campo para decidir como conferir a assinatura está confiando na entrada do atacante para saber como validar a entrada do atacante.

O exploit clássico coloca "alg": "none", remove a assinatura inteira e manda um payload afirmando o que quiser. Uma biblioteca que respeite o cabeçalho vê "nenhum algoritmo", não faz verificação nenhuma e aceita.

Uma variante troca RS256 por HS256. No RSA o servidor verifica com uma chave pública; no HMAC ele verifica com um segredo compartilhado. Se o verificador troca conforme o cabeçalho, um atacante pode assinar um token usando a chave pública como segredo do HMAC — pública, então ele tem — e o servidor valida.

Os dois se corrigem do mesmo jeito: fixe o algoritmo esperado no servidor e ignore o que o token afirma. Toda biblioteca madura suporta isso; normalmente é um argumento que você precisa passar, não um padrão que você ganha.

Decodificar não é verificar

Isso merece título próprio porque é o erro que transforma um mal-entendido em vulnerabilidade.

Decodificar mostra o conteúdo. Não diz nada sobre a assinatura ser válida. Um token adulterado decodifica com exatamente a mesma limpeza de um legítimo — troque "role": "user" por "role": "admin", recodifique, e o decodificador vai te mostrar um token de admin.

Só quem tem a chave de assinatura consegue distinguir. O que significa:

  • Verificação pertence ao servidor, sempre.
  • Um cliente pode decodificar um token para ler um nome de exibição ou uma expiração para fins de interface. Nunca para tomar decisão de autorização.
  • Qualquer endpoint que confie numa claim sem verificar a assinatura está sem autenticação, por mais que a requisição pareça autenticada.

Um checklist curto de projeto

  • Coloque em sub a menor coisa que identifique o sujeito, e nada que você se incomodaria de vazar.
  • Use um exp curto. Minutos para tokens de acesso, e um mecanismo separado de refresh para longevidade.
  • Fixe o algoritmo no verificador. Rejeite none explicitamente.
  • Compare exp em segundos.
  • Se você realmente precisa do payload escondido, o que você quer é JWE — tokens criptografados — não um token assinado com as partes sensíveis ofuscadas. Base64 não é ofuscação, e nunca foi.

Um JWT é um cartão-postal assinado. Perfeitamente bom para o que faz; só não escreva nele nada que você não queira que o carteiro leia.