Aprenda / Segurança web

IntermediárioAula 16 de 1625 min0% da trilha

Projeto guiado: endurecendo uma API vulnerável

Uma API de pedidos fictícia, cheia de falhas discutidas nesta trilha, corrigida uma a uma — fechando fundamentos e intermediário juntos.

Esta é a API fictícia de pedidos que vamos herdar: login por sessão, um endpoint pra ver o próprio pedido, e uma seção de comentários no pedido. Ela "funciona" — passa em todo teste manual óbvio — e tem pelo menos quatro problemas que essa trilha já explicou em profundidade, espalhados por ela. Vamos encontrar e corrigir cada um, na ordem em que apareceriam numa revisão de segurança de verdade.

1. Cookie de sessão sem nenhum atributo de proteção

O login está funcionando — usuário manda credencial, servidor cria sessão e devolve o cookie (lição 2). O problema mora nos atributos que faltam (lição 3):

Antes
cookies["sessao"] = sessao.id
# gera: Set-Cookie: sessao=abc123
Depois
cookies["sessao"] = {
  value: sessao.id,
  httponly: true,   # JS não lê — reduz o estrago de um XSS que roube cookie
  secure: true,      # só trafega em HTTPS
  same_site: :lax,   # bloqueia o cenário comum de CSRF (POST disparado por outro site)
}

Sem esses três atributos, o cookie estava simultaneamente exposto a roubo via XSS (se existisse algum, e existe — problema 4 desta lista), a interceptação em rede sem HTTPS, e a CSRF. Nenhum dos três exige mudança de arquitetura — só configuração correta no momento de criar o cookie.

2. IDOR no endpoint de pedido

O endpoint que devolve o pedido buscava só pelo id, sem checar dono (lição 10):

Antes
def show
  pedido = Pedido.find(params[:id])   # qualquer pedido, de qualquer usuário
  render json: pedido
end
Depois
def show
  pedido = current_usuario.pedidos.find(params[:id])  # escopo restrito ao dono
  render json: pedido
end
# id de outro usuário: RecordNotFound → 404, sem revelar que o pedido existe

A correção restringe a própria query ao usuário atual, em vez de buscar livremente e confiar que ninguém vai testar um id alheio — a mesma diferença entre autenticação (quem você é, já garantida antes desse trecho) e autorização (o que você pode acessar, que faltava completamente aqui).

3. Login sem limite de tentativas

O endpoint de login aceitava tentativas ilimitadas, sem penalidade nenhuma (lição 11):

Antes
def create
  usuario = Usuario.find_by(email: params[:email])
  if usuario&.authenticate(params[:senha])
    iniciar_sessao(usuario)
  else
    render json: { error: "Credenciais inválidas" }, status: :unauthorized
  end
end
# nenhum limite — um script pode tentar milhares de senhas por minuto
Depois
class Rack::Attack
  throttle("login/email", limit: 5, period: 60) do |req|
    req.params.dig("email")&.downcase if req.path == "/login" && req.post?
  end
end
# depois de 5 tentativas em 60s pro mesmo email, novas tentativas
# recebem 429 antes mesmo de chegar no controller

O limite entra numa camada antes do controller — o próprio controller de login não muda nada em si, o throttle intercepta a requisição antes dela ser processada. Isso é intencional: separar "quem pode tentar fazer login agora" de "a lógica de verificar a senha em si" evita misturar as duas responsabilidades no mesmo método.

4. Comentário do pedido renderizado sem sanitizar

O frontend renderizava o comentário do pedido direto como HTML, sem tratamento (lição 7):

Antes
function ComentarioPedido({ texto }) {
  return <div dangerouslySetInnerHTML={{ __html: texto }} />;
}
# um comentário salvo como "<script>roubaCookie()</script>" executa
# na tela de QUALQUER pessoa que abrir esse pedido
Depois
function ComentarioPedido({ texto }) {
  return <div>{texto}</div>;  // renderização normal já escapa HTML
}
# "<script>" vira texto literal na tela — nunca é interpretado

Como o comentário não precisava de nenhuma formatação rica (negrito, link), a correção mais simples e mais segura foi parar de usar dangerouslySetInnerHTML — voltando pra renderização padrão do React, que já escapa conteúdo dinâmico por conta própria.

O que essas quatro correções têm em comum

Nenhuma delas exigiu reescrever arquitetura, trocar de framework ou adicionar uma dependência pesada nova. Cada uma foi um ajuste pontual — um atributo de cookie, uma cláusula a mais numa query, uma regra de throttle, a remoção de um dangerouslySetInnerHTML desnecessário. É exatamente o ponto da lição 1: segurança raramente é sobre grandes decisões de arquitetura tomadas uma vez — é sobre um conjunto de hábitos aplicados de forma consistente em cada decisão pequena.

Exercício

A mesma API tem um quinto problema, ainda não listado: o endpoint de busca de pedidos por descrição usa Pedido.where("descricao LIKE '%#{params[:busca]}%'"). Identifique a vulnerabilidade, qual lição desta trilha a cobre, e reescreva a linha corrigida.

Quiz rápido

Por que a correção do IDOR (current_usuario.pedidos.find em vez de Pedido.find) não exigiu nenhuma biblioteca nova?
O que as quatro correções desta lição guiada têm em comum, segundo o fechamento da lição?