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.
- Leia o corpo original e os headers necessários.
- Valide X-Hub-Signature-256.
- Leia X-GitHub-Delivery e X-GitHub-Event.
- Registre a entrega de modo atômico, respeitando a chave única do GUID.
- Enfileire somente o trabalho permitido pelo estado atual.
- Responda 2XX.
- 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.




