ArquiteturaSegurançaWebhooksStripeBubble

Seu webhook não é a verdade. É apenas um aviso.

Qualquer um pode te enviar esse aviso, e o que você faz depois de recebê-lo é o que separa um sistema protegido de um desprotegido.

15 min de leitura Galde Cordeiro Silvestre

Neste artigo
  1. O que é um webhook?
  2. O que o evento prova?
  3. Quando o remetente é desconhecido
  4. Reconhecer a origem não é autenticar a mensagem
  5. O que o provedor te oferece
  6. A conferência que decide: assinatura
  7. Autenticar não é saber o que aconteceu
  8. Quando não dá para confirmar
  9. Quanto isso custa?
  10. Quanto isso custa no Bubble?

Pense na seguinte ilustração.

O que é um webhook?

Um webhook é um aviso automático que um sistema manda para outro. Um pagamento entrou, um boleto venceu, um documento foi assinado, uma entrega saiu para rota. Alguma coisa aconteceu lá, e o seu sistema fica sabendo aqui, sem precisar ficar perguntando de minuto em minuto.

Portanto, o webhook é como se fosse a ligação da ilustração. O endereço do webhook é o seu número, o que recebe a ligação. E assim como você escolhe o seu número quando vai comprar um chip, você cria o endereço do webhook no seu sistema e o fornece para o provedor, que passa a avisá-lo a cada acontecimento. Quem faz a chamada é o provedor (ou deveria ser), e o seu sistema é quem recebe.

Só que esse endereço não é secreto. Ele pode ficar escrito na configuração do provedor, aparecer em logs e em documentação, e pode vazar como vaza um número de telefone.

Por isso, aviso não é veredito, e quase todo problema de webhook vem de esquecer essa diferença: "atender" e já fazer o que a "voz do outro lado" pediu. Chega um evento de pagamento, o sistema lê, marca a assinatura como paga, libera a conta e manda o e-mail de boas-vindas. Funciona e continua funcionando por meses, até a manhã em que alguém tem acesso sem nunca ter pagado, e ninguém consegue explicar como.

O que o evento prova?

Da mesma forma, o webhook avisa, mas atender não é confiar: quem confirma é a fonte.

O sistema faz o que ouviu Provedor depagamento o pagamento passou Seu sistema Contaliberada Ninguém conferiu quem falava. O sistema agiu sobre uma afirmação. O sistema liga de volta Provedor depagamento Seu sistema alguma coisa aconteceu, talvez ligando de volta: como está esse pagamento? a resposta da fonte pagolibera a conta estornadonão faz nada Quem decide é a resposta, e uma das duas respostas é não fazer nada.
A mesma ligação, dois sistemas. O de baixo liga de volta no número oficial, e é a resposta que decide o que acontece: às vezes liberar a conta, às vezes não fazer nada.

Vale um caso concreto: o cliente paga, se arrepende e pede estorno no mesmo minuto, e isso são duas ligações, dois avisos. Se o seu servidor estiver lento, ou a fila congestionar, elas podem chegar fora de ordem. O sistema que acredita no que recebeu libera a conta no segundo aviso, e ela continua liberada mesmo com o pagamento estornado. O sistema que pergunta de volta recebe "estornado" nas duas vezes, e não faz nada, como deveria.

Quando o remetente é desconhecido

Ignorar a ligação. O sistema recusa o evento e não guarda nada sobre ele. É aceitável quando o custo de estar errado é baixo.

Atender só para saber quem era, e desligar sem fazer nada. Isso tem nome em software: é um registro (ou log). Você anota que o evento chegou, de que endereço veio e a que horas, e não age sobre nada. Parece pouco, mas é o que salva a investigação de três semanas depois, quando a pergunta for "isso já tinha acontecido antes?".

A terceira decisão é a que não se defende, e é justamente a que os sistemas escolhem: fazer o que a voz pediu.

Reconhecer a origem não é autenticar a mensagem

Em software, com relação aos webhooks, são os mesmos três problemas. O endereço de origem pode ser forjado, o conteúdo da mensagem pode ter sido trocado no caminho, e o segredo que autentica a chamada pode ter vazado.

É aqui que as defesas improvisadas mostram o seu poder, mas também as suas limitações. Elas aparecem em qualquer linguagem e em qualquer ferramenta, porque todas nascem da mesma vontade de não lidar com criptografia ou por falta de conhecimento mais amplo. São cinco, e quase sempre aparecem juntas.

Criar um endereço que ninguém adivinha (segurança por obscuridade). Um nome aleatório e comprido, apostando que ninguém vai conseguir acertá-lo. Mas muitos se esquecem de que ele provavelmente vai estar disponível em vários lugares: a documentação pública, o Swagger, o histórico do navegador, o log do servidor.

Adicionar um segredo na URL (token na query string). O endereço carrega um parâmetro aleatório, então só quem o conhece consegue enviá-lo e só quem o conhece consegue checá-lo ao receber. É como se fosse uma senha ou código pré-combinado entre quem envia e quem recebe, e não divulgado para mais ninguém. Mas lembre-se: ele pode ser armazenado de uma forma não tão segura e pode acabar vazando ou sendo interceptado.

Aceitar só endereços conhecidos (IP allowlist). Alguns usam uma lista de IPs conhecidos: boa como segunda camada, ruim como única. A lista muda, a infraestrutura do provedor muda, e você descobre quando os processos param.

Conferir apenas se o evento existe (consulta por ID). O sistema pega o ID da mensagem e pergunta ao provedor se aquilo é real. É melhor que as anteriores, porque é a única que conversa com a fonte. Mas isso responde "essa ligação existiu", e não "quem está falando é quem diz ser".

Recusar o que chegou atrasado (timestamp freshness, a conferência do horário). O sistema olha o horário que vem dentro da mensagem e descarta o que for velho. Isso ajuda contra reenvio acidental e contra a repetição de um evento guardado, mas o horário vem dentro da própria mensagem: quem forja a mensagem escreve o horário também. Ela só vira defesa de verdade quando esse horário está dentro da assinatura, que é o assunto da próxima seção.

Nenhuma dessas camadas é inútil, e usadas em conjunto elas até trazem algum benefício: empilhar defesas tem nome, é defesa em profundidade, que é o nome de "não apostar tudo numa camada só". Mas nenhuma delas é suficiente sozinha, e o erro é usar uma delas como a fechadura.

Prova quem mandou Prova que nada mudou Verificação de assinatura recalcule sobre o corpo bruto e compare HMAC Endpoint com nome obscuro obscuridade não é fronteira segurança por obscuridade Token na URL viaja em log e em captura de tela token na query string Lista de IPs permitidos boa segunda camada, a lista muda IP allowlist O ID do evento existe confirma que é real, não quem mandou consulta por ID Recusar o que chegou atrasado o horário vem dentro da mensagem timestamp freshness
Uma conferência e cinco hábitos. Só a primeira responde às duas perguntas, e é por isso que ela é a que decide.

O que o provedor te oferece

As cinco de cima são o que os desenvolvedores improvisam quando acham que não tem escolha. Mas quase todo provedor sério oferece alguma verificação de verdade, e vale saber o que cada uma prova antes de escolher.

O que o provedor ofereceO que isso prova
Cabeçalho fixo ou Basic auth, configurados por vocêQue quem chamou tem a credencial. Nada sobre o conteúdo nem sobre a idade da mensagem.
Assinatura HMAC num cabeçalho (Stripe, GitHub, Shopify, Slack, Twilio)Autoria e integridade. Com o horário dentro da assinatura, também a idade.
Assinatura assimétrica ou JWT assinado (PayPal, Apple, Google)O mesmo, conferido com chave pública, então nem o seu lado consegue forjar uma mensagem.
Conteúdo criptografado, que você decifra do seu lado (Branch)Que quem escreveu tinha a chave, e que ninguém no caminho leu o conteúdo.
mTLS, com certificado do lado de quem chamaQue a conexão veio de alguém com um certificado que você aceita.

Duas linhas dessa tabela costumam ser confundidas com as outras, e a confusão muda a escolha.

A primeira é a credencial. Ela responde a uma pergunta só: "quem chamou tem a chave?" A assinatura responde a outra: "esta mensagem é exatamente esta mensagem, e quem a escreveu tinha o segredo". Uma não cobre a outra. Se a chave aparece num log e alguém a copia, essa pessoa inventa um evento de pagamento aprovado de dez mil reais e manda com a credencial certa, porque a credencial não diz nada sobre o conteúdo que vem atrás dela. Com assinatura, esse conteúdo inventado não fecha com o segredo e a mensagem é recusada.

A segunda é o conteúdo criptografado. Cifrar é embaralhar a mensagem de um jeito que só quem tem a chave consegue desembaralhar. Isso impede que alguém no caminho leia o que está escrito, e impedir de ler não é o mesmo que impedir de mexer. São duas garantias diferentes: uma é sigilo, a outra é integridade. Na prática elas quase sempre vêm juntas, porque o jeito moderno de cifrar já acusa qualquer alteração, e isso tem nome, é criptografia autenticada. Mas é o normal, não é garantia: se o provedor só cifra, confira a assinatura também, quando ele oferecer uma.

E tem uma linha que não é do provedor, é sua: se o sistema sempre liga de volta antes de agir, uma ligação falsa custa uma consulta inútil, e não uma decisão errada. Isso se chama reconciliação, não substitui a assinatura, e diminui muito o estrago de qualquer coisa que passe pelas conferências.

A conferência que decide: assinatura

Em software isso é a verificação de assinatura, e o mecanismo tem um nome que aparece na documentação de todo provedor que a usa: HMAC.

O provedor faz uma conta que mistura a mensagem exata que está mandando com uma chave secreta que só vocês dois conhecem, e manda o resultado junto. Do seu lado, a mesma conta sobre a mesma mensagem, com a mesma chave secreta, tem que dar o mesmo resultado. Se der, duas coisas ficam resolvidas de uma vez: (1) quem falou sabe a chave secreta; (2) a mensagem chegou intacta, nenhum caractere do que foi escrito mudou no caminho. Na assinatura assimétrica a ideia é a mesma, mudando só quem guarda a chave.

Portanto, essa é outra natureza de conferência. As defesas anteriores diminuem o risco, e esta é a única que prova as duas coisas, quem mandou e que nada mudou, e é por isso que ela é mais crítica, confiável e fica acima de todas as outras.

E são dois detalhes principais que decidem se ela funciona:

Ela confere o texto como ele chegou. Qualquer coisa que tente reescrever ou alterar a mensagem no caminho faz a comparação falhar numa mensagem perfeitamente legítima, e o sintoma é sempre o mesmo: a conferência reprova o que é verdadeiro, e ninguém entende por quê. Mas basicamente a comparação falha porque houve modificação intencional ou não da mensagem.

Ela não tem prazo de validade. Uma assinatura válida continua sendo válida para sempre.

Isso pode permitir um ataque chamado de ataque de replay, ou seja, usar uma chamada validada no passado para tentar executar um processo no futuro. A defesa é a mesma conferência do horário da lista anterior, o timestamp freshness, que aqui finalmente vale alguma coisa: o horário está dentro da assinatura, e para ela continuar válida, o horário não pode ser alterado, lembra?! Portanto, quem forjar a mensagem precisa manter o mesmo conteúdo, incluindo o mesmo horário. O que você escolhe é a janela de tolerância, que é quanto tempo será permitido a um webhook chegar com atraso. Alguns provedores reconhecem que pode haver algum atraso e até mesmo já sugerem esse tempo em suas documentações. As bibliotecas oficiais do Stripe, por exemplo, vêm com cinco minutos de tolerância, e a documentação avisa para não baixar isso para zero: zero parece o ajuste mais rigoroso de todos, e desliga a conferência de idade inteira.

Autenticar não é saber o que aconteceu

Três hábitos cuidam do resto.

Confirme na fonte. Antes de qualquer coisa que envolva principalmente dinheiro ou acesso, busque o objeto no provedor pelo ID e aja sobre o que voltar. Em software isso se chama reconciliação. O evento diz que um pagamento aconteceu; a consulta confirma o estado atual do pagamento, se realmente aconteceu ou se foi revertido dois minutos atrás. E tem um segundo motivo, menos lembrado: nem sempre a ordem de chegada dos eventos é garantida, e às vezes o cancelamento de uma assinatura chega antes da atualização que ainda dizia ativa. Quem grava o estado a partir do evento deixa o último que chegou vencer, mesmo tendo sido o primeiro a acontecer. Portanto, a resposta da fonte é a única coisa que não depende da ordem.

Espere a mesma coisa chegar duas vezes. Isso é rotina, e não defeito: o provedor reenvia quando o seu servidor demora a responder e a rede entrega a mesma mensagem duas vezes, ou mesmo durante um processo de configuração e teste. Isso se chama idempotência: rodar a mesma operação duas vezes deixa o sistema exatamente como ele ficou da primeira. Quem torna isso possível é o identificador do evento, guardado na chegada, chamado também de chave de idempotência: a segunda chegada encontra esse registro e não faz nada, por exemplo, impede de cobrar o cartão de novo. Além disso, num caso de idempotência, sempre responda que deu certo, mesmo sem fazer nada: recusar uma repetição com erro avisa o provedor de que o evento falhou, e ele reenvia de novo, que é exatamente o contrário do que você quer.

Registre o que aconteceu. Pode ser no início e no fim do processo, ou o caminho inteiro. Um registro (ou log) por evento, escrito no instante em que ele chega e atualizado a cada conferência, dependendo do caso de uso. Esse registro é a sua trilha de auditoria. Você não deveria querer saber somente se o processo "falhou?", mas "por que falhou?", "até onde ele foi?" e "no que ele acreditou quando parou?", e isso um log que só guarda sucesso não responde.

Quando não dá para confirmar

O último princípio parece errado até o dia em que ele salva você. Quando uma conferência não consegue decidir, por falta de dados ou algo inconsistente, o restante do fluxo não deveria fazer nada além de apenas registrar o problema para ser verificado.

Deixar que o fluxo continue seria seguir como se tivesse dado certo, como se uma conta não paga tivesse sido efetivada. Recusar em silêncio é pior ainda, porque silêncio é idêntico a servidor fora do ar, e ninguém investiga um problema que nem sabe que existe, um sistema que nunca reclama.

O evento parado e registrado também tem nome, dead letter queue, que é só um canto onde a mensagem que não pôde ser tratada aguarda a revisão e a decisão de uma pessoa, em vez de sumir. Ele fica ali com contexto suficiente para rodar de novo quando a causa estiver resolvida.

Quanto isso custa?

Nenhuma das etapas de segurança listadas no quadro a seguir é mirabolantemente inventada, e tudo tem nome: verificação HMAC, timestamp freshness, reconciliação, idempotência, trilha de auditoria, fail closed, entre outros. São etapas que devem se tornar hábito, e que geralmente vão estar presentes quando a integração é pagamento, entrega, assinatura de documento ou qualquer outra coisa que te mande um evento e desligue.

E sim, configurar tudo de maneira correta dá mais trabalho do que apenas receber a chamada, ler a mensagem, ignorar silenciosamente qualquer erro ou seguir em frente, mas essa é uma das principais diferenças entre um sistema correto, seguro e auditável quando comparado com um sistema que está ignorando problemas silenciosos e prestes a identificar uma vulnerabilidade da forma mais dolorosa possível.

Webhook é aviso, não veredito, e é preciso ter cautela.

Quanto isso custa no Bubble?

No Bubble, de modo normal, se você decidir usar as principais camadas de segurança nativamente possíveis, incluindo as opcionais, serão basicamente oito camadas, já que a assinatura HMAC não existe nativamente no Bubble.

As oito deverão ser escritas e mantidas por você, e cada ponto de falha só chega à sua trilha de auditoria se houver um fluxo a mais escrito para registrar aquele ponto.

As quatro de criticidade alta, a assinatura (HMAC), a conferência do horário (timestamp freshness), a idempotência (idempotency) e a reconciliação (reconciliation), não são alternativas entre si: tirar uma não economiza trabalho, mas sim abre de novo a brecha que ela fechava. As demais são um reforço, e um sistema pode viver sem elas.

No entanto, alguns usuários usam o segredo na URL como alternativa à assinatura HMAC, achando que ela é 100% segura, mas na verdade ela é extremamente vulnerável, principalmente quando usada sozinha, e isso acontece em muitos casos por falta de conhecimento ou descuido com as camadas de segurança.

Já outros usam a reconciliação do evento para se certificar de que o evento existe e do seu estado atual. Isso é realmente uma boa prática, mas não impede que um evento forjado dispare a reconciliação desnecessariamente ou que execute outros processos em cadeia se houver outras falhas de verificação ou validação no fluxo.

Com o plugin Webhook Sentinel (Stripe), cinco dessas conferências passam a ser feitas por ele e uma deixa de ser necessária. Em outras duas ele não decide, mas entrega o que elas precisam: o identificador do evento, que é a chave usada para checar a idempotência, e o identificador do objeto, que é usado para a consulta de reconciliação. O resto continua sendo seu.

Além disso, os registros recomendados deixam de ser uma decisão de cada etapa, pois o plugin retorna a razão específica da falha, permitindo que você configure sua trilha de auditoria em um único ponto do fluxo, depois da verificação, em vez de um ponto a cada conferência que podia falhar.

Configuração recomendada:

Nove conferências em fila, todas escritas e mantidas por você.

O plugin cobre cinco, aposenta uma, e facilita duas, entregando o identificador que elas precisam. O resto continua sendo seu.

O evento chega nada foi conferido ainda A assinatura confere? sobre o corpo bruto, antes do parsing HMAC Alta o plugin cobre Não existe nativamente no Bubble não O segredo da URL confere? a assinatura já cobre token na query string Baixa não precisa mais não É recente? seis horas depois, continua válida timestamp freshness Alta o plugin cobre não É novo? a segunda chegada não faz nada idempotência Alta o plugin facilita não A fonte confirma? aja sobre a resposta, não sobre o evento reconciliação Alta o plugin facilita não O IP é da lista? recusa antes de gastar trabalho IP allowlist Média o plugin cobre não Tem o que eu preciso? o ID, a referência, o ambiente validação mínima Opcional o plugin cobre não Veio do ambiente certo? teste não mexe em produção environment isolation Opcional o plugin cobre não Está dentro do limite? enxurrada, e não repetição rate limiting Opcional não Continuar o fluxo liberar, cobrar, enviar regra de negócio Registro (recomendado) opcional em cada ponto, e sem ele a recusa some cinco cobertas, um motivo só trilha de auditoria assinatura inválidasegredo erradofora da janelajá vistoa fonte discordaIP recusadodados insuficientesambiente trocadolimite estourado Encerrar o fluxo nada é feito fail closed
  • Altanão se negocia
  • Médiareforço que vale
  • Opcionalescolha sua
  • Baixaquase não conta
O fluxo completo, com a criticidade de cada conferência. Nada continua sem passar pelas anteriores, e nada que para some em silêncio.

Qualquer uma dessas conferências, sozinha, pode levar alguns segundos ou minutos para ser escrita manualmente, mas não é esse o ponto principal. O ponto principal é o fato de não haver nativamente a verificação de assinatura HMAC, e outro ponto é reusabilidade e manutenção. Quantas dessas verificações continuarão existindo seis meses depois, em todos os webhooks que o app tiver, em todos os apps que o usuário gerenciar?