Skip to content
This page has been auto-translated and may contain errors.View in English

Como pensar sobre segurança

Uma funcionalidade pode funcionar perfeitamente e ainda assim não ser segura. Imagine uma pequena rota de pedidos:

js
app.get('/orders/:id', async (req, res) => {
  const order = await db.orders.findById(req.params.id)
  res.json(order)
})

A rota lê um id, busca um pedido e o devolve. Se você testar com o seu próprio pedido, tudo parece certo. Agora mude o id na URL. Se o servidor nunca verifica quem é o dono daquele pedido, um cliente autenticado consegue ler o recibo de outro cliente.

Essa é a mentalidade de segurança: não pergunte apenas se o código funciona. Pergunte o que alguém poderia fazer ele fazer.

JunoComo pensar sobre segurança Código que funciona responde a uma pergunta: essa funcionalidade faz o que queríamos?

Segurança faz uma segunda pergunta: o que acontece quando alguém a usa de um jeito que não pretendíamos?

JunoComo pensar sobre segurança O bug na rota de pedidos não é um bug de JavaScript. O handler roda, a consulta retorna, e a resposta é um JSON válido.

O movimento de revisão certo é nomear a decisão em voz alta: esse código verifica se um pedido existe, mas não se este usuário pode lê-lo. Uma vez que você consegue nomear a decisão que falta, você consegue escrever o teste que falta.

JunoComo pensar sobre segurança A maioria dos bugs de segurança em aplicações são caminhos comuns do produto com um chamador hostil na sala. O caminho do produto continua funcionando, e é por isso que o teste do caminho feliz sorri para você enquanto o relatório de incidente afia os dentes.

Leia a funcionalidade como afirmações sob pressão: a URL afirma um id, a sessão afirma um usuário, o corpo afirma uma intenção. O servidor precisa decidir quais afirmações se tornam fatos.

Perceba a fronteira

Uma fronteira de confiança é a linha entre o código que você controla e algo que pode te enviar um valor que você não escolheu.

O navegador fica do outro lado dessa linha. Um usuário pode mudar a URL, editar um campo de formulário oculto, repetir um parâmetro de consulta, remover um cookie ou pular seu front-end e enviar uma requisição manualmente.

Isso faz do navegador o lugar errado para tomar decisões que importam para você.

Verificações no navegador são úteis, mas não são o controle

A validação do lado do cliente ajuda as pessoas a preencherem formulários. Ela pega erros de digitação cedo e proporciona uma experiência melhor.

O servidor ainda precisa verificar o valor de novo, porque é o servidor que protege os dados.

Aqui está a mesma rota de pedidos com a decisão que faltava agora explícita:

js
app.get('/orders/:id', async (req, res) => {
  const order = await db.orders.findById(req.params.id)

  if (!order || order.userId !== req.user.id) {
    return res.status(404).json({ error: 'Order not found' })
  }

  res.json(order)
})

A linha importante é a comparação entre order.userId e req.user.id. O servidor está dizendo: esse pedido pode existir, mas também precisa pertencer à pessoa que está pedindo por ele.

Uma requisição é permitida quando o servidor consegue provar a decisão por conta própria.

JunoPerceba a fronteira Trate o navegador como um visitante numa recepção. Ele pode pedir coisas, mas não decide o que tem permissão para ver.

Coloque as verificações importantes no servidor. É lá que seu app pode comparar a requisição com os dados em que já confia.

JunoPerceba a fronteira Uma fronteira de confiança é onde um valor cruza de um lugar que você não controla para dentro do código que vai tomar uma decisão.

Um atalho útil de revisão: liste todos os valores que o handler lê e marque quais vieram do chamador. Qualquer coisa escolhida pelo chamador precisa de uma regra do lado do servidor antes de afetar dados, identidade, permissões ou carga de trabalho.

JunoPerceba a fronteira As fronteiras vão além das bordas de rede. Jobs em segundo plano, ferramentas administrativas, webhooks, configurações de ambiente e serviços internos podem carregar valores vindos de algum lugar que seu código atual não escolheu.

A linha pertence a qualquer ponto onde sua disposição de absorver a falha de outro sistema muda. Uma vez que você a desenha, você deve a esse cruzamento uma verificação, um teste, um caminho de erro e, para algum pobre engenheiro no futuro, uma pista sobre o motivo de ela existir.

Nomeie o que pode quebrar

Depois de identificar uma decisão arriscada, dê um nome à possível falha:

PerguntaTermo de segurançaExemplo
Quem pode ler isso?ConfidencialidadeUm cliente lê o pedido de outro cliente.
Quem pode mudar isso?IntegridadeUm cliente muda o preço antes de finalizar a compra.
As pessoas ainda conseguem usar isso?DisponibilidadeUma requisição deixa o app lento para todo mundo.

Juntas, essas propriedades são chamadas de tríade CIA: confidencialidade, integridade e disponibilidade.

Um recibo vazado é um problema de confidencialidade. Um campo de preço oculto é um problema de integridade. Uma rota de busca que permite que uma requisição peça um milhão de registros é um problema de disponibilidade.

Alguns bugs afetam mais de uma propriedade. Se um cliente pode editar o endereço de entrega de outro cliente, ele consegue tanto vê-lo quanto mudá-lo.

JunoNomeie o que pode quebrar Use as três perguntas antes dos três nomes. Quem pode ler isso? Quem pode mudar isso? As pessoas ainda conseguem usar isso?

Essas perguntas transformam uma preocupação vaga em algo que você consegue explicar. Um relatório de bug fica mais claro quando você consegue nomear o dano.

JunoNomeie o que pode quebrar Confidencialidade, integridade e disponibilidade ajudam você a escolher a correção certa.

Um vazamento de leitura precisa de uma verificação de acesso. Uma mudança feita pelo usuário errado precisa de autoridade do lado do servidor. Uma rota custosa precisa de limites de tamanho, taxa ou tempo. Mesma rota, propriedade diferente, reparo diferente.

JunoNomeie o que pode quebrar As três propriedades expõem as trocas que as pessoas preferem não registrar por escrito. O bloqueio de conta protege a confidencialidade contra tentativas de adivinhação, mas dá aos atacantes uma forma de bloquear usuários reais.

Disponibilidade é a propriedade que as equipes mais costumam sacrificar de propósito. Registre essa troca no design, ou alguém vai redescobri-la durante uma interrupção, com café mais frio e menos opções.

Pense pelo outro lado

A frase "pense como um atacante" pode soar dramática. No dia a dia do desenvolvimento, ela é mais discreta. Escolha uma entrada e pergunte:

  • E se esse valor estiver ausente?
  • E se ele for muito maior do que o esperado?
  • E se ele for do tipo errado?
  • E se ele pertencer a outro usuário?
  • E se a mesma requisição for enviada milhares de vezes?

Pegue um formulário de perfil. A página pode mostrar um campo de texto para displayName, um userId oculto e um botão de enviar. O caminho pretendido é pequeno: a pessoa autenticada atualiza o próprio nome.

O caminho da segurança faz perguntas diferentes. Por que o userId está no formulário, afinal? O que acontece se ele apontar para outra pessoa? O servidor usa a sessão autenticada, ou ele confia no campo enviado?

Oculto não significa confiável

Um campo de formulário oculto está escondido da página, não da pessoa que envia a requisição.

Se o valor importa, derive-o no servidor a partir da sessão ou do banco de dados. Não peça para o navegador devolver uma decisão que o servidor já conhece.

O mesmo hábito funciona fora dos formulários. Um upload de arquivo afirma um nome de arquivo e um tipo de conteúdo. Um token afirma uma identidade. Um webhook afirma que um evento aconteceu. Verifique a afirmação no momento em que seu código está prestes a depender dela.

JunoPense pelo outro lado Comece com um campo só. Pergunte o que acontece se ele estiver ausente, for longo demais, tiver o formato errado, pertencer a outra pessoa ou for enviado repetidas vezes.

Isso já é pensamento de segurança, em uma forma pequena e utilizável.

JunoPense pelo outro lado A versão útil de "pensar como um atacante" é pensar nas entradas. Leia o handler e marque cada afirmação que o chamador faz: identidade, id, papel, preço, nome de arquivo, URL de retorno, tipo de conteúdo.

Depois pergunte onde cada afirmação é verificada. Se uma afirmação afeta uma decisão antes que essa verificação aconteça, você tem um formato de bug que vale a pena testar.

JunoPense pelo outro lado Classifique as perguntas pela facilidade de alcance. Um bug que um usuário comum e autenticado consegue disparar com uma única requisição merece atenção antes de um que exija timing preciso, acesso interno ou uma flag de deployment antiga de 2019.

Isso não torna os casos extremos inofensivos. Só mantém a revisão focada nos caminhos que atacantes de verdade, usuários entediados e scanners automatizados vão tentar antes do almoço.

Para onde isso vai a seguir

Essa mentalidade é a preparação para a próxima parte do curso.

A modelagem de ameaças pergunta o que poderia dar errado antes que uma funcionalidade seja lançada. O OWASP dá a você nomes compartilhados para bugs em aplicações em produção. A triagem de bugs ajuda você a decidir o que corrigir primeiro.

Mantenha o ciclo pequeno:

  1. Encontre a fronteira.
  2. Nomeie o que pode quebrar.
  3. Verifique a afirmação no servidor.