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):
cookies["sessao"] = sessao.id
# gera: Set-Cookie: sessao=abc123cookies["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):
def show
pedido = Pedido.find(params[:id]) # qualquer pedido, de qualquer usuário
render json: pedido
enddef 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 existeA 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):
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 minutoclass 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 controllerO 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):
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 pedidofunction ComentarioPedido({ texto }) {
return <div>{texto}</div>; // renderização normal já escapa HTML
}
# "<script>" vira texto literal na tela — nunca é interpretadoComo 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.