JWT em Profundidade: Algoritmos, Vulnerabilidades Conhecidas e Defesas na Prática Já leu

Fundamentos de JWT: O Que Realmente É JWT (JSON Web Token) é um padrão aberto (RFC 7519) para transmitir informações de forma segura entre duas partes. Contrário ao que muitos pensam, JWT não é um método de criptografia, mas sim uma estrutura de serialização e assinatura de dados. A segurança dele vem da capacidade de verificar se o token foi alterado ou forjado, não de ocultar o conteúdo. Um JWT é composto por três partes separadas por pontos: header.payload.signature. O header define qual algoritmo foi usado, o payload contém as claims (dados), e a signature garante a autenticidade. Quando você decodifica um JWT em um site como jwt.io, você consegue ler o payload sem precisar de nenhuma chave — isso é normal e esperado. O que torna o token válido é sua assinatura, que apenas o servidor consegue gerar com a chave secreta. Anatomia Prática de um JWT Vamos analisar um token real: Decodificando cada parte: Header (base64url decoded): Payload

Fundamentos de JWT: O Que Realmente É

JWT (JSON Web Token) é um padrão aberto (RFC 7519) para transmitir informações de forma segura entre duas partes. Contrário ao que muitos pensam, JWT não é um método de criptografia, mas sim uma estrutura de serialização e assinatura de dados. A segurança dele vem da capacidade de verificar se o token foi alterado ou forjado, não de ocultar o conteúdo.

Um JWT é composto por três partes separadas por pontos: header.payload.signature. O header define qual algoritmo foi usado, o payload contém as claims (dados), e a signature garante a autenticidade. Quando você decodifica um JWT em um site como jwt.io, você consegue ler o payload sem precisar de nenhuma chave — isso é normal e esperado. O que torna o token válido é sua assinatura, que apenas o servidor consegue gerar com a chave secreta.

Anatomia Prática de um JWT

Vamos analisar um token real:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Decodificando cada parte:

Header (base64url decoded):

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

Payload (base64url decoded):

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

A signature é gerada pela função HMAC-SHA256 aplicada sobre header.payload usando uma chave secreta. Qualquer alteração no header ou payload invalida a signature.

Gerando e Verificando um JWT em Python

import jwt
import json
from datetime import datetime, timedelta

# Chave secreta (em produção, use variáveis de ambiente)
SECRET_KEY = "sua_chave_super_secreta_aqui"

# Gerando um token
payload = {
    "sub": "usuario123",
    "name": "João Silva",
    "email": "joao@example.com",
    "iat": datetime.utcnow(),
    "exp": datetime.utcnow() + timedelta(hours=1)
}

token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
print(f"Token gerado: {token}")

# Verificando e decodificando o token
try:
    decoded = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
    print(f"Token válido! Dados: {decoded}")
except jwt.ExpiredSignatureError:
    print("Token expirou")
except jwt.InvalidSignatureError:
    print("Assinatura inválida - token foi alterado")
except jwt.DecodeError:
    print("Erro ao decodificar token")

Se alguém tentar alterar o payload sem conhecer a chave secreta, a verificação falhará imediatamente. Essa é a proteção fundamental que JWT oferece.

Algoritmos de Assinatura: Escolhas e Implicações

Existem diferentes algoritmos que podem ser usados para assinar JWT. A escolha afeta diretamente a segurança e o desempenho do sistema. Os algoritmos se dividem principalmente em dois grupos: HMAC (simétricos) e RSA/ECDSA (assimétricos).

Algoritmos HMAC (HS256, HS384, HS512)

HMAC usa uma única chave compartilhada entre servidor e cliente. Todos que possuem a chave podem tanto gerar quanto verificar tokens. Isso é eficiente, mas problemático em arquiteturas distribuídas onde múltiplos servidores precisam validar tokens.

import jwt
from datetime import datetime, timedelta

SECRET = "chave_compartilhada"

# Gerando com HMAC
token_hmac = jwt.encode(
    {"user_id": 42, "exp": datetime.utcnow() + timedelta(hours=1)},
    SECRET,
    algorithm="HS256"
)

# Verificando com mesma chave
decoded = jwt.decode(token_hmac, SECRET, algorithms=["HS256"])
print(f"Usuário: {decoded['user_id']}")

A vulnerabilidade crítica aqui é que se a chave vazar, qualquer pessoa consegue criar tokens válidos se passando por qualquer usuário. Use HMAC apenas em cenários onde a chave pode ser mantida segura.

Algoritmos RSA (RS256, RS384, RS512)

RSA usa um par de chaves: uma privada (para assinar) e uma pública (para verificar). O servidor assina com a chave privada e os clientes/servidores verificam com a chave pública. Isso permite que múltiplos sistemas validem tokens sem conhecer a chave privada.

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
import jwt
from datetime import datetime, timedelta

# Gerando par de chaves RSA (normalmente feito uma única vez)
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
    backend=default_backend()
)
public_key = private_key.public_key()

# Salvando chaves em PEM (em produção, use arquivo seguro ou HSM)
private_pem = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)
public_pem = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# Assinando token com chave privada
payload = {"user_id": 42, "exp": datetime.utcnow() + timedelta(hours=1)}
token = jwt.encode(payload, private_pem, algorithm="RS256")
print(f"Token assinado: {token}")

# Verificando com chave pública
decoded = jwt.decode(token, public_pem, algorithms=["RS256"])
print(f"Validado! User ID: {decoded['user_id']}")

RSA é mais seguro para ambientes distribuídos, mas mais lento. Escolha conforme sua arquitetura: HMAC para serviço único, RSA para microsserviços.

Algoritmos ECDSA (ES256, ES384, ES512)

ECDSA (Elliptic Curve Digital Signature Algorithm) oferece segurança similar a RSA com chaves menores e operações mais rápidas. É a escolha moderna recomendada.

from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
import jwt
from datetime import datetime, timedelta

# Gerando par de chaves ECDSA
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()

# Serializando
private_pem = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)
public_pem = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# Usando com JWT
payload = {"user_id": 99, "exp": datetime.utcnow() + timedelta(hours=1)}
token = jwt.encode(payload, private_pem, algorithm="ES256")
decoded = jwt.decode(token, public_pem, algorithms=["ES256"])
print(f"Token ECDSA validado: {decoded}")

Vulnerabilidades Conhecidas e Ataques Reais

JWT ganhou fama negativa não porque o padrão seja fraco, mas porque implementações ingênuas abrem portas para ataques sofisticados. Vamos examinar as vulnerabilidades mais críticas que exploram má implementação.

Ataque 1: Algorithm Confusion (CVE-2016-9565)

O servidor implementa verificação com RSA, mas o cliente consegue forçar o algoritmo para HMAC. Como o servidor usa a chave pública RSA como "chave secreta" HMAC (o que é um erro grave), o cliente consegue assinar tokens válidos porque conhece a chave pública.

import jwt

# Cenário vulnerável: servidor com chave pública RSA
PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Z3VS5JJcds3P3Ib...
-----END PUBLIC KEY-----"""

# Atacante consegue a chave pública (é pública mesmo!)
# E envia um token assinado com HMAC usando a chave pública como segredo
forged_token = jwt.encode(
    {"user_id": 1, "is_admin": True},
    PUBLIC_KEY,
    algorithm="HS256"  # HMAC ao invés de RS256
)

# Servidor vulnerável aceita porque não valida o algoritmo esperado
try:
    decoded = jwt.decode(forged_token, PUBLIC_KEY, algorithms=["HS256", "RS256"])
    print(f"TOKEN FORJADO ACEITO: {decoded}")
except jwt.InvalidSignatureError:
    print("Assinatura inválida")

Defesa: Sempre especifique explicitamente qual algoritmo esperar:

# CORRETO - Especificar apenas o algoritmo esperado
decoded = jwt.decode(
    token, 
    public_key, 
    algorithms=["RS256"]  # Não aceita HS256
)

Ataque 2: None Algorithm (CVE-2015-9235)

O servidor aceita tokens assinados com o algoritmo "none", criado para casos onde não há assinatura. Um atacante consegue criar qualquer token sem chave.

import jwt
import json
import base64

# Criando um token com algoritmo "none" manualmente
header = base64.urlsafe_b64encode(
    json.dumps({"alg": "none", "typ": "JWT"}).encode()
).decode().rstrip('=')

payload = base64.urlsafe_b64encode(
    json.dumps({"user_id": 999, "is_admin": True}).encode()
).decode().rstrip('=')

# Token sem assinatura
forged_token = f"{header}.{payload}."

# Servidor vulnerável que aceita "none"
try:
    decoded = jwt.decode(forged_token, "", algorithms=["none"])
    print(f"VULNERÁVEL! Token aceito: {decoded}")
except jwt.InvalidAlgorithmError:
    print("Algoritmo 'none' não aceito")

Defesa: Nunca acumule algoritmos. Rejeite "none" explicitamente:

# CORRETO
ALLOWED_ALGORITHMS = ["RS256", "RS384", "RS512"]

decoded = jwt.decode(
    token,
    public_key,
    algorithms=ALLOWED_ALGORITHMS  # "none" não está na lista
)

Ataque 3: Expiração Não Verificada

Muitos desenvolvedores extraem dados do JWT sem validar se expirou. O token pode estar tecnicamente inválido, mas o código não verifica.

import jwt
from datetime import datetime, timedelta

SECRET = "segredo"

# Token com expiração no passado
old_payload = {
    "user_id": 42,
    "exp": datetime.utcnow() - timedelta(hours=1)  # Expirou há 1 hora!
}
old_token = jwt.encode(old_payload, SECRET, algorithm="HS256")

# Código vulnerável - decodifica sem validar
def vulnerable_decode(token):
    # NÃO FAZA ISSO
    decoded = jwt.decode(token, SECRET, algorithms=["HS256"], options={"verify_exp": False})
    return decoded["user_id"]

# Código correto
def secure_decode(token):
    try:
        decoded = jwt.decode(token, SECRET, algorithms=["HS256"])
        return decoded["user_id"]
    except jwt.ExpiredSignatureError:
        print("Token expirou - recuse acesso")
        return None

Defesa: Sempre valide expiração (é o padrão de jwt.decode):

# PyJWT valida expiração por padrão
decoded = jwt.decode(token, SECRET, algorithms=["HS256"])
# Se expirou, lança jwt.ExpiredSignatureError

Ataque 4: Injeção de Claims Arbitrárias

Um atacante pode adicionar claims falsas ao payload, alterando seu comportamento. Embora a assinatura seja inválida, implementações fracas podem usar o token parcialmente.

import jwt
import json
import base64

SECRET = "segredo"

# Token legítimo
legitimate = jwt.encode({"user_id": 42}, SECRET, algorithm="HS256")

# Decodificando partes manualmente
header_b64, payload_b64, signature_b64 = legitimate.split('.')

# Modificando payload para adicionar is_admin
payload_decoded = json.loads(
    base64.urlsafe_b64decode(payload_b64 + '==')
)
payload_decoded["is_admin"] = True

# Tentando reconstruir (assinatura será inválida)
malicious = f"{header_b64}.{base64.urlsafe_b64encode(json.dumps(payload_decoded).encode()).decode().rstrip('=')}.{signature_b64}"

# Se a aplicação usar opt-out de validação...
try:
    decoded = jwt.decode(malicious, SECRET, algorithms=["HS256"], options={"verify_signature": False})
    print(f"VULNERÁVEL: {decoded}")  # Mostra o claim falso
except jwt.InvalidSignatureError:
    print("Assinatura inválida - token recusado")

Defesa: Sempre valide assinatura (é o padrão). Nunca use verify_signature=False:

# CORRETO - Validar SEMPRE
decoded = jwt.decode(token, SECRET, algorithms=["HS256"])
# Se assinatura for inválida, lança exceção

Implementação Segura: Padrões e Melhores Práticas

Agora que conhecemos os ataques, vamos implementar um sistema JWT robusto em um cenário real: autenticação de API.

Estrutura Completa com Refresh Tokens

from flask import Flask, jsonify, request
import jwt
from datetime import datetime, timedelta
from functools import wraps
import os

app = Flask(__name__)

# Configurações seguras
ACCESS_TOKEN_EXPIRY = timedelta(minutes=15)
REFRESH_TOKEN_EXPIRY = timedelta(days=7)
ALGORITHM = "HS256"
SECRET_KEY = os.environ.get("JWT_SECRET_KEY", "NUNCA_USE_ISSO_EM_PRODUCAO")

# Simular banco de dados de usuários
USERS = {
    "joao": {"password": "hash_seguro_aqui", "id": 1}
}

# Armazem simples de refresh tokens (usar Redis em produção)
REFRESH_TOKENS_BLACKLIST = set()

def create_access_token(user_id, username):
    """Cria token de acesso com expiração curta."""
    payload = {
        "sub": user_id,
        "username": username,
        "type": "access",
        "iat": datetime.utcnow(),
        "exp": datetime.utcnow() + ACCESS_TOKEN_EXPIRY
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def create_refresh_token(user_id, username):
    """Cria token de refresh com expiração longa."""
    payload = {
        "sub": user_id,
        "username": username,
        "type": "refresh",
        "iat": datetime.utcnow(),
        "exp": datetime.utcnow() + REFRESH_TOKEN_EXPIRY
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def verify_token(token, token_type="access"):
    """Verifica validade e tipo do token."""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])

        # Validações rigorosas
        if payload.get("type") != token_type:
            return None, "Tipo de token inválido"

        return payload, None

    except jwt.ExpiredSignatureError:
        return None, "Token expirou"
    except jwt.InvalidSignatureError:
        return None, "Assinatura inválida"
    except jwt.DecodeError:
        return None, "Token malformado"

def token_required(f):
    """Decorator para proteger rotas com autenticação."""
    @wraps(f)
    def decorated(*args, **kwargs):
        token = None

        # Extrair token do header Authorization
        if "Authorization" in request.headers:
            auth_header = request.headers["Authorization"]
            try:
                token = auth_header.split(" ")[1]
            except IndexError:
                return jsonify({"error": "Token inválido"}), 401

        if not token:
            return jsonify({"error": "Token ausente"}), 401

        payload, error = verify_token(token, token_type="access")

        if error:
            return jsonify({"error": error}), 401

        # Passar usuário para a função
        return f(payload, *args, **kwargs)

    return decorated

@app.route("/auth/login", methods=["POST"])
def login():
    """Endpoint de login que retorna access e refresh tokens."""
    data = request.get_json()

    if not data or not data.get("username") or not data.get("password"):
        return jsonify({"error": "Username e password necessários"}), 400

    # Validar credenciais (em produção, verificar hash com bcrypt)
    user = USERS.get(data["username"])
    if not user:
        return jsonify({"error": "Credenciais inválidas"}), 401

    # Gerar tokens
    access_token = create_access_token(user["id"], data["username"])
    refresh_token = create_refresh_token(user["id"], data["username"])

    return jsonify({
        "access_token": access_token,
        "refresh_token": refresh_token,
        "token_type": "Bearer"
    }), 200

@app.route("/auth/refresh", methods=["POST"])
def refresh():
    """Endpoint para renovar access token usando refresh token."""
    data = request.get_json()
    refresh_token = data.get("refresh_token")

    if not refresh_token:
        return jsonify({"error": "Refresh token ausente"}), 401

    if refresh_token in REFRESH_TOKENS_BLACKLIST:
        return jsonify({"error": "Refresh token revogado"}), 401

    payload, error = verify_token(refresh_token, token_type="refresh")

    if error:
        return jsonify({"error": error}), 401

    # Gerar novo access token
    new_access = create_access_token(payload["sub"], payload["username"])

    return jsonify({
        "access_token": new_access,
        "token_type": "Bearer"
    }), 200

@app.route("/api/profile", methods=["GET"])
@token_required
def get_profile(user_payload):
    """Rota protegida que requer token válido."""
    return jsonify({
        "id": user_payload["sub"],
        "username": user_payload["username"],
        "message": "Dados sensíveis aqui"
    }), 200

@app.route("/auth/logout", methods=["POST"])
@token_required
def logout(user_payload):
    """Logout revogando refresh token (implementação mínima)."""
    data = request.get_json()
    refresh_token = data.get("refresh_token")

    if refresh_token:
        REFRESH_TOKENS_BLACKLIST.add(refresh_token)

    return jsonify({"message": "Logout realizado"}), 200

if __name__ == "__main__":
    app.run(debug=False, ssl_context="adhoc")

Validação de Segurança em Profundidade

def secure_jwt_config():
    """Configurações de segurança obrigatórias."""
    return {
        # 1. Algoritmos explícitos (nunca aceitar "none")
        "algorithms": ["HS256"],

        # 2. Validações obrigatórias
        "verify_signature": True,
        "verify_exp": True,
        "verify_nbf": True,
        "verify_iat": True,
        "verify_aud": False,

        # 3. Requisitos de claims
        "require": ["exp", "iat", "sub"]
    }

# Uso rigoroso
def authenticate(token):
    return jwt.decode(
        token,
        SECRET_KEY,
        algorithms=["HS256"],
        options=secure_jwt_config()
    )

Conclusão

Três aprendizados fundamentais você leva deste artigo:

  1. JWT é assinatura, não criptografia. O token é legível, mas sua integridade é garantida pela signature. Isso não é fraqueza, é design. Use JWT para autenticação e autorização, não para proteger dados sensíveis dentro do payload — para isso, use criptografia simetria adicional se necessário.

  2. Implementação insegura mata a segurança do padrão. Algoritmo "none", algoritmo confusion, e desvalidação de expiração não são falhas de JWT, mas erros humanos de implementação. Validação rigorosa é obrigatória: sempre especifique algoritmos explícitos, sempre valide expiração, sempre rejeite tokens modificados.

  3. Arquitetura importa na escolha do algoritmo. HMAC é rápido mas centralizado; RSA/ECDSA é distribuído mas lento. Combine com refresh tokens curtos e rotação de chaves periódica. Não é apenas validar — é construir defesa em camadas.

Referências


Artigos relacionados