Trilha 13 · Segurança e criptografia

Autenticação na web

Você já tem hash, cifra, chave pública e TLS. Nada disso impede que a credencial vaze pela porta da frente — um script injetado numa página, um formulário disparado por outro site, uma senha reutilizada que apareceu num vazamento alheio. Esta lição fecha a trilha onde a segurança de verdade costuma falhar: na aplicação.

① Intuição

Uma pulseira de festival

HTTP não tem memória: cada requisição chega como se fosse a primeira. Provar quem você é a cada clique — digitando a senha de novo — seria insuportável. A solução é a pulseira de festival: você mostra o documento uma vez na entrada e recebe algo que, dali em diante, vale como identidade.

Toda a discussão sobre autenticação web decorre disso. A pulseira pode ser um número que só faz sentido para o portão (sessão) ou um crachá que já traz seus dados impressos e um selo (JWT). E, como qualquer pulseira, ela pode ser roubada — o que muda tudo é onde ela fica guardada e o que a impede de ser usada por outra pessoa.

Autenticação e autorização são coisas diferentes. Autenticação responde "quem é você?"; autorização responde "você pode fazer isto?". Confundi-las produz uma das falhas mais comuns em APIs: o servidor confirma que o token é válido — autenticação correta — e esquece de verificar se aquele usuário pode ver aquele recurso. Trocar /pedidos/1234 por /pedidos/1235 e ver o pedido de outra pessoa é a falha de autorização mais banal e mais frequente que existe.
② Visualização interativa

Sessão vs JWT, e os ataques na prática

Na primeira aba, compare os dois modelos e clique em "revogar o acesso" — é aí que a diferença aparece. Na segunda, combine o local de armazenamento do token com cada ataque e veja que nenhuma escolha vence tudo.

HTTP não tem memória: cada requisição chega sozinha, sem saber quem você é. Autenticação é resolver isso — e há duas famílias de solução, com um trade-off que costuma ser mal compreendido.

💻 NAVEGADOR guarda
Cookie: sessao=a3f9c2e1
um identificador opaco, sem significado nenhum — 32 bytes aleatórios
🖥️ SERVIDOR guarda
a3f9c2e1 → {"sub":"42","nome":"Ana","exp":"2026-07-23T15:00"}
uma tabela de sessões, em memória ou no Redis
a cada requisição:
1. o servidor recebe o id da sessão
2. busca na tabela de sessões ← consulta a cada requisição
3. sabe quem é o usuário
Dois enganos frequentes sobre JWT. Primeiro: o payload é apenas base64, não criptografia — qualquer um lê o conteúdo colando o token num decodificador. A assinatura impede alterar, não ler; nunca coloque dado sensível ali. Segundo: JWT não é "mais seguro" que sessão. Ele troca uma consulta de estado por perda de revogação imediata. Para uma aplicação web comum com um banco de dados, a sessão costuma ser a escolha mais simples e mais segura; o JWT brilha quando vários serviços independentes precisam validar sem chamar um serviço central.
③ Explicação técnica

Sessões no servidor

# HTTP não tem memória. Cada requisição chega sozinha, sem saber
# quem você é. Autenticação na web é resolver isso.

# --- ABORDAGEM 1: SESSÃO NO SERVIDOR ---
# O cookie guarda um identificador OPACO, sem significado nenhum.
# O estado real fica no servidor.

sessoes["a3f9c2e1..."] = {"user_id": 42, "exp": ...}

Set-Cookie: sessao=a3f9c2e1;
    HttpOnly;              # JavaScript NÃO consegue ler → barra XSS
    Secure;                # só viaja por HTTPS
    SameSite=Lax;          # não vai junto em requisições de outro site → barra CSRF
    Path=/; Max-Age=86400

# O id precisa vir de um gerador CRIPTOGRAFICAMENTE seguro:
import secrets
secrets.token_urlsafe(32)      # ✓
# random.random() é previsível — quem observar alguns ids consegue
# prever os próximos e sequestrar sessões alheias.

# ✓ revogação instantânea: basta apagar a linha
# ✗ exige consultar o estado a cada requisição

JWT, e os dois enganos clássicos

# --- ABORDAGEM 2: JWT (token autocontido) ---
# O token CARREGA os dados e uma assinatura. O servidor não guarda nada.

# Três partes separadas por ponto:
#   header.payload.assinatura
#   eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiI0MiJ9 . k3NfVv8...

#   header:  {"alg": "HS256", "typ": "JWT"}
#   payload: {"sub": "42", "exp": 1753280000}
#   assinatura: HMAC(chave_secreta, header + "." + payload)

# ⚠ ENGANO Nº 1: o payload é BASE64, não criptografia.
# Qualquer pessoa lê o conteúdo colando o token em jwt.io.
# A assinatura impede ALTERAR, não LER. Nunca ponha dado sensível ali.

# ⚠ ENGANO Nº 2: aceitar o algoritmo que o token declara.
# O header vem do ATACANTE. A vulnerabilidade clássica: enviar
# {"alg": "none"} e uma assinatura vazia. Bibliotecas antigas
# aceitavam. Sempre fixe o algoritmo esperado no seu código:
jwt.decode(token, chave, algorithms=["HS256"])   # nunca deduza do header

# ✓ sem consulta de estado: valida com CPU apenas
# ✗ NÃO DÁ PARA REVOGAR — o token vale até expirar sozinho
#   Mitigações: expiração curta (5-15 min) + refresh token, ou
#   uma lista de revogação — que reintroduz exatamente a consulta
#   de estado que o JWT prometia eliminar.

# Quando o JWT vale a pena: vários serviços independentes que
# precisam validar sem chamar um serviço central.
# Para um app web comum com um banco de dados: sessão é mais
# simples e mais segura. "Stateless" não é sinônimo de "melhor".
Onde guardar o token: a resposta honesta é "depende, e não existe escolha perfeita". No localStorage, qualquer JavaScript da página — inclusive o de uma dependência npm comprometida — consegue lê-lo e exfiltrá-lo; em compensação, ele não viaja sozinho numa requisição forjada, o que o imuniza a CSRF. Num cookie HttpOnly, o JavaScript não o alcança, mas o navegador o anexa automaticamente, o que exige SameSite e token anti-CSRF. A prática recomendada hoje é o cookie HttpOnly + Secure + SameSite=Lax: o XSS continua sendo grave, mas a credencial não sai da máquina.

Os ataques que realmente acontecem

# XSS — Cross-Site Scripting
# Código do atacante executa DENTRO da sua página, com todos os
# privilégios do seu domínio.
#   ✗ elemento.innerHTML = entrada_do_usuario
#   ✓ elemento.textContent = entrada_do_usuario
#   ✓ frameworks (React, Vue) escapam por padrão — o perigo mora
#     nas escotilhas: dangerouslySetInnerHTML, v-html
#   ✓ Content-Security-Policy limita o que pode executar
#   ✓ cookie HttpOnly: se o XSS acontecer mesmo assim, ao menos
#     a credencial não pode ser EXFILTRADA para uso posterior

# CSRF — Cross-Site Request Forgery
# Outro site faz seu navegador enviar uma requisição autenticada.
# Explora a gentileza do navegador de anexar cookies sozinho.
#   ✓ SameSite=Lax (padrão nos navegadores atuais)
#   ✓ token anti-CSRF nos formulários
#   ✓ conferir o cabeçalho Origin

# FORÇA BRUTA e CREDENTIAL STUFFING
# O segundo é mais comum e mais eficaz: testar e-mails e senhas
# vazados de OUTRO site, apostando no reúso de senha.
#   ✓ rate limiting por IP e por conta
#   ✓ verificar a senha contra listas de vazamentos conhecidos
#   ✓ MFA — corta praticamente todo esse ataque

# ENUMERAÇÃO DE USUÁRIOS
#   ✗ "e-mail não cadastrado" vs "senha incorreta"
#      revela quais e-mails existem no sistema
#   ✓ mensagem idêntica nos dois casos, e tempo de resposta também
#     (calcule o hash mesmo quando o usuário não existe)

# TIMING ATTACK
#   ✗ if token_recebido == token_esperado   ← para no 1º byte diferente
#   ✓ hmac.compare_digest(a, b)             ← tempo constante

OAuth, MFA e passkeys

# OAUTH 2.0 / OPENID CONNECT — "entrar com o Google"
#
# OAuth é sobre AUTORIZAÇÃO (dar acesso limitado a um recurso);
# OpenID Connect é a camada de AUTENTICAÇÃO em cima dele.
# Confundir os dois é a origem de várias falhas conhecidas.
#
# O fluxo (authorization code + PKCE):
#   1. o app manda o usuário ao Google
#   2. o usuário se autentica LÁ — o app nunca vê a senha
#   3. o Google devolve um código de uso único ao app
#   4. o app troca o código por um token, pelo canal de trás
#
# O ganho: o app nunca toca na senha e recebe só o escopo pedido.
# Use uma biblioteca — implementar OAuth do zero é um campo minado
# de detalhes (validar state, nonce, aud, iss, expiração...).

# MFA — o segundo fator, em ordem de força:
#   SMS      — fraco: vulnerável a troca de chip (SIM swap)
#   TOTP     — bom: código de 6 dígitos, derivado de um segredo
#              compartilhado + o relógio. Funciona offline.
#   WebAuthn — o melhor: resistente a PHISHING por construção

# PASSKEYS (WebAuthn) — o fim da senha
#   Um par de chaves POR SITE. A privada nunca sai do dispositivo
#   (fica no Secure Enclave / TPM), liberada por biometria.
#   No login, o site manda um desafio e o dispositivo o assina.
#
#   . não há segredo compartilhado → um vazamento no servidor
#     não expõe nada de útil
#   . o navegador só assina para o DOMÍNIO verdadeiro → o phishing
#     simplesmente para de funcionar, mesmo com o usuário enganado
#
# É a criptografia de chave pública desta trilha aplicada ao login —
# exatamente o mesmo princípio das chaves SSH, com melhor usabilidade.

# E a regra que vale mais que todas: NÃO IMPLEMENTE AUTENTICAÇÃO
# DO ZERO. Use o provedor da sua plataforma ou uma biblioteca
# madura e auditada.
④ Projeto para programar

Experimentando

Mini projeto: implemente login com sessão do zero num framework simples (Flask, Express). Use secrets.token_urlsafe(32) para o id, bcrypt para a senha e todas as flags de cookie. Depois quebre-o de propósito: troque o gerador por random.random() e mostre que, observando alguns ids, dá para prever os próximos; tire o HttpOnly e leia o cookie pelo console do navegador. Ver a falha funcionar fixa a defesa muito melhor que a recomendação.

Projeto principal: monte um laboratório de XSS e CSRF. Um "fórum" propositalmente vulnerável em que comentários são inseridos com innerHTML, e uma página atacante noutra porta. Escreva o comentário que rouba o token do localStorage e o envia para o seu servidor. Depois aplique as defesas uma a uma — textContent, CSP, cookie HttpOnly, SameSite — e verifique qual ataque cada uma para. Faça isso apenas na sua máquina, contra a sua própria aplicação.

Desafio extra: implemente TOTP (RFC 6238) do zero — são cerca de 20 linhas: um HMAC do contador de tempo, truncado a 6 dígitos. Gere o QR code, cadastre no seu app autenticador de celular e confira que os códigos batem. Depois lide com o detalhe que a especificação não deixa óbvio: a tolerância de relógio (aceitar a janela anterior e a seguinte) e a proibição de reusar um código já consumido. Em seguida, experimente o WebAuthn no navegador com navigator.credentials.create() e compare as duas experiências.

⑤ Exercícios rápidos

Teste sua intuição

Qual é a principal desvantagem do JWT em relação a sessões no servidor?
O que a flag HttpOnly num cookie faz?
O payload de um JWT é secreto?
Por que passkeys (WebAuthn) são resistentes a phishing?
⑥ Aplicações no mundo real

Onde você encontra isso

🔁

Credential stuffing e o reúso de senha

A maior parte das invasões de conta hoje não quebra senha nenhuma: usa combinações de e-mail e senha vazadas de outro serviço, apostando no reúso. Bilhões de credenciais circulam em listas públicas. É por isso que gerenciadores de senha e MFA valem mais, na prática, que qualquer regra de complexidade — e por que exigir "uma maiúscula e um símbolo" resolve pouco se a senha é a mesma de sempre.

🎣

Phishing resistente por construção

Em 2017 o Google passou a exigir chaves de segurança físicas dos seus funcionários e relatou zero comprometimentos por phishing desde então. O motivo é estrutural, não educacional: mesmo que a pessoa caia no site falso, o navegador não produz assinatura para um domínio que não seja o registrado. É a diferença entre treinar humanos para não errar e desenhar um sistema em que o erro não tem consequência.

📋

OWASP Top 10

A lista das dez classes de falha mais críticas em aplicações web, atualizada periodicamente. As primeiras posições são consistentemente ocupadas por falhas de controle de acesso — não por criptografia quebrada. É o resumo desta lição: o elo mais fraco quase nunca é a matemática, e sim a aplicação que esqueceu de verificar se aquele usuário podia mesmo acessar aquele recurso.

← Anterior: TLS e HTTPS Concluir trilha: Segurança e criptografia →