Programação na Prática22/09/2026Redação de Tecnologia e Desenvolvimento6 min de leituraAtualizado em 26/09/2026

Webhook do GitHub: como validar HMAC, impedir replay e processar eventos duplicados

Guia prático para autenticar webhooks do GitHub com HMAC-SHA256, usar X-GitHub-Delivery contra replay, deduplicar eventos e processar reentregas com idempotência.

Ilustração em fundo escuro mostra fluxo de webhooks do GitHub com verificação de assinatura, proteção contra replay, fila, cache e descarte de evento duplicado.

Validar um webhook do GitHub exige tratar três problemas diferentes: confirmar que a entrega veio do GitHub e não foi alterada, impedir que a mesma entrega produza efeitos repetidos e responder rápido o bastante para não transformar processamento lento em falha de entrega. A combinação prática é HMAC sobre o corpo original, deduplicação pelo X-GitHub-Delivery, estado idempotente no banco e uma fila para tirar o trabalho pesado do caminho da resposta HTTP.

O que cada mecanismo resolve

Mecanismo Problema que resolve Ponto de implementação
HMAC-SHA256 Autenticidade e integridade da entrega Recalcular a assinatura com o secret e comparar com X-Hub-Signature-256
X-GitHub-Delivery Identidade da entrega e proteção contra replay Persistir o GUID e rejeitar ou absorver repetições conforme o estado
Idempotência Evitar efeito de negócio duplicado Usar chave única, transação ou operação que possa ser repetida com segurança
Fila Evitar timeout no endpoint Validar, registrar, enfileirar e devolver 2XX rapidamente

1. Valide o HMAC antes de interpretar o JSON

Quando um webhook tem um secret configurado, o GitHub envia X-Hub-Signature-256. A assinatura começa com sha256= e é calculada com HMAC-SHA256 usando o secret como chave e o conteúdo do payload como mensagem. A documentação recomenda validar a assinatura antes de continuar o processamento.

O detalhe que mais causa erro é o corpo usado no cálculo. A assinatura depende do conteúdo recebido. Se o framework analisar o JSON e depois o serializar novamente, espaços, ordem de propriedades ou codificação podem mudar. Por isso, capture o corpo original da requisição e calcule o HMAC antes de qualquer transformação. O próprio exemplo oficial em Ruby lê request.body antes de chamar o parser, e o exemplo em TypeScript lê o texto da requisição antes de validar.

Exemplo verificável em Node.js

const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");

const a = Buffer.from(expected, "utf8"); const b = Buffer.from(signatureHeader || "", "utf8"); const valid = a.length === b.length && crypto.timingSafeEqual(a, b);

O teste de tamanho antes de timingSafeEqual é necessário porque a API do Node.js exige buffers com o mesmo comprimento. Se o header estiver ausente, usar outro prefixo ou não coincidir com o HMAC calculado, a entrega deve ser tratada como não autenticada e não deve chegar à lógica de negócio.

O GitHub também publica um vetor de teste oficial: com o secret It's a Secret to Everybody e o payload Hello, World!, o valor esperado de X-Hub-Signature-256 começa com sha256=757107ea.... Esse teste é útil para confirmar a implementação antes de conectar o endpoint ao webhook real.

2. Use X-GitHub-Delivery para replay e deduplicação

O header X-GitHub-Delivery é um GUID que identifica a entrega. O GitHub recomenda usá-lo para proteger o endpoint contra replay. Há um detalhe essencial: quando uma entrega é reenviada de forma intencional, o mesmo X-GitHub-Delivery é preservado. Isso significa que o identificador serve ao mesmo tempo para deduplicar repetições indesejadas e reconhecer uma reentrega legítima da mesma entrega.

Modele estado, não apenas uma lista de IDs bloqueados

Uma tabela de recebimentos pode usar delivery_id como chave única e guardar campos como status, received_at, attempt_count e last_error. Um fluxo robusto pode seguir estas regras:

  • Se o GUID nunca foi visto, crie o registro em estado received e enfileire o trabalho.
  • Se o GUID já está em completed, devolva 2XX sem repetir o efeito de negócio.
  • Se o GUID está em processing, evite criar um segundo consumidor concorrente para a mesma entrega.
  • Se o GUID está em failed, uma reentrega pode abrir uma nova tentativa controlada, sem gerar duas operações simultâneas.

Esse desenho é melhor do que simplesmente inserir o GUID em uma blacklist permanente no primeiro contato. Uma entrega pode ter sido recebida, mas falhar depois na fila. Como a reentrega mantém o mesmo GUID, bloquear para sempre impediria uma recuperação legítima.

3. Faça a operação de negócio ser idempotente

Deduplicar o GUID reduz repetições, mas não substitui idempotência. Há janelas de falha em que o consumidor pode concluir uma operação e cair antes de marcar o registro como concluído. Em uma nova tentativa, o mesmo trabalho pode aparecer novamente.

A proteção mais forte fica perto do efeito de negócio. Exemplos: usar uma chave única (delivery_id, operation), gravar o evento e a alteração de estado na mesma transação, ou usar uma chave natural do domínio que impeça a mesma mutação duas vezes. Para integrações externas, guarde a chave de idempotência associada ao efeito enviado, quando a API de destino oferecer esse recurso.

4. Valide, registre, enfileire e responda

O GitHub orienta que o servidor devolva uma resposta 2XX em até 10 segundos. Se o endpoint exceder esse tempo, a conexão é encerrada e a entrega é considerada uma falha. Por isso, não é uma boa ideia executar sincronamente chamadas externas, geração de relatórios ou tarefas demoradas antes da resposta.

  1. Leia o corpo original e os headers necessários.
  2. Valide X-Hub-Signature-256.
  3. Leia X-GitHub-Delivery e X-GitHub-Event.
  4. Registre a entrega de modo atômico, respeitando a chave única do GUID.
  5. Enfileire somente o trabalho permitido pelo estado atual.
  6. Responda 2XX.
  7. No consumidor, execute a lógica idempotente e atualize o estado para completed ou failed.

O header X-GitHub-Event informa o tipo do evento. O GitHub também recomenda checar o tipo e, quando houver, a ação do payload antes de processar. Isso evita que um endpoint genérico aplique regras erradas a eventos inesperados.

5. Reentrega intencional precisa ser parte do desenho

O GitHub não reenvia automaticamente entregas que falharam. O operador pode solicitar reentrega pela interface ou pela API, e a documentação permite reentregar entregas recentes dos últimos três dias. Como a reentrega conserva o mesmo GUID, o consumidor deve decidir pelo estado se há trabalho pendente ou se a entrega já foi concluída.

Um padrão seguro é: completed significa resposta 2XX sem repetir efeito; failed permite nova tentativa controlada; processing exige lock, lease ou outra proteção contra concorrência; estados antigos presos em processing podem ser recuperados por timeout operacional bem definido.

Fluxo de referência

Etapa Verificação Resultado
Entrada HTTPS, método esperado e limites de tamanho Requisição aceita para autenticação
Assinatura HMAC-SHA256 do corpo original e comparação em tempo constante Origem e integridade validadas
Identidade X-GitHub-Delivery GUID associado ao estado persistido
Roteamento X-GitHub-Event e action Handler correto selecionado
Fila Inserção idempotente Resposta 2XX rápida
Consumidor Transação, chave única ou chave de idempotência Efeito de negócio ocorre no máximo uma vez do ponto de vista lógico

Erros comuns

  • Calcular o HMAC sobre JSON reserializado em vez do corpo recebido.
  • Comparar assinatura com == em vez de uma rotina de comparação apropriada.
  • Confiar apenas no IP de origem e dispensar o secret. A allow list pode ser uma camada adicional, não um substituto do HMAC.
  • Usar somente memória local para deduplicação em uma aplicação com várias instâncias.
  • Marcar um GUID como processado antes de o efeito de negócio realmente concluir.
  • Responder 2XX antes de registrar de forma durável o trabalho que precisa ser processado.
  • Tratar toda repetição do mesmo GUID como ataque e, com isso, bloquear uma reentrega legítima de uma tentativa anterior que falhou.

Checklist de implementação

  • Secret aleatório de alta entropia armazenado fora do código.
  • X-Hub-Signature-256 validado antes do parse e da lógica de negócio.
  • Corpo original preservado para o cálculo do HMAC.
  • Comparação de assinatura resistente a timing.
  • X-GitHub-Delivery com restrição única e estado persistido.
  • Handler selecionado por X-GitHub-Event e ação.
  • Fila durável e resposta 2XX dentro do limite operacional.
  • Operação do consumidor idempotente.
  • Política clara para estados processing, failed e completed.
  • Procedimento de reentrega e observabilidade para falhas.

Fontes

Tópicos deste artigofilasGitHubHMACidempotênciaNode.jssegurançawebhookswebhook do GitHubvalidar HMAC GitHubX-GitHub-Deliveryimpedir replay webhookeventos duplicados GitHubidempotência webhook