Como validar webhooks do GitHub com HMAC em PHP e Node.js
Receber um webhook é simples: você expõe um endpoint, recebe um POST e interpreta o JSON. O problema começa quando esse endpoint confia em qualquer payload que chega. Se uma aplicação executa ações a partir de webhooks - atualiza dados, inicia deploys, envia notificações ou dispara automações - ela precisa confirmar duas coisas antes de processar o evento: - o payload foi assinado com o secret compartilhado com o GitHub; - o conteúdo recebido não foi modificado no caminho. Neste artigo, vamos implementar essa verificação com HMAC SHA-256 em PHP e Node.js. Os exemplos completos e testados estão no repositório github-webhook-security-guide. Como a assinatura funciona Ao enviar uma entrega, o GitHub calcula um HMAC usando o corpo original da requisição, o secret configurado no webhook e o algoritmo SHA-256. O resultado é enviado no header: X-Hub-Signature-256: sha256= Seu endpoint precisa calcular a assinatura esperada a partir do corpo bruto recebido e compará-la com o valor do header. Esse detalhe é importante: não valide a assinatura depois de converter o JSON em objeto e serializá-lo novamente. Qualquer mudança nos bytes - espaços, quebras de linha, Unicode ou ordem de serialização - muda o HMAC. Checklist mínimo - Leia o corpo bruto antes de interpretar o JSON. - Rejeite secret vazio. - Valide o formato de X-Hub-Signature-256 . - Calcule o HMAC com SHA-256. - Compare em tempo constante. - Rejeite assinaturas ausentes ou inválidas. - Só então interprete e processe o evento. Implementação em PHP 8+ Em PHP, podemos usar hash_hmac() para gerar a assinatura e hash_equals() para realizar a comparação segura: <?php declare(strict_types=1); function verifyGitHubWebhook( string $payload, string $signatureHeader, string $secret ): bool { if ( $secret === '' || !preg_match('/^sha256=[a-f0-9]{64}$/', $signatureHeader) ) { return false; } $expected = 'sha256=' . hash_hmac( 'sha256', $payload, $secret ); return hash_equals($expected, $signatureHeader); } O formato é verificado antes do cálculo. Isso elimina headers ausentes ou malformados e garante que a comparação receba valores com a estrutura esperada. Um endpoint genérico poderia usar a função assim: $payload = file_get_contents('php://input'); $signature = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? ''; $secret = getenv('GITHUB_WEBHOOK_SECRET') ?: ''; if (!verifyGitHubWebhook($payload, $signature, $secret)) { http_response_code(401); exit('Invalid signature'); } $event = json_decode($payload, true, flags: JSON_THROW_ON_ERROR); O secret vem do ambiente, nunca do repositório. Implementação em Node.js 20+ No Node.js, usamos createHmac() e timingSafeEqual() do módulo nativo node:crypto : import { createHmac, timingSafeEqual } from "node:crypto"; export function verifyGitHubWebhook(payload, signatureHeader, secret) { if (!secret || !/^sha256=[a-f0-9]{64}$/.test(signatureHeader ?? "")) { return false; } const expected = sha256=${createHmac("sha256", secret) .update(payload) .digest("hex")}; const receivedBuffer = Buffer.from(signatureHeader, "utf8"); const expectedBuffer = Buffer.from(expected, "utf8"); return ( receivedBuffer.length === expectedBuffer.length && timingSafeEqual(receivedBuffer, expectedBuffer) ); } A verificação do tamanho antes de timingSafeEqual() é obrigatória: a função lança uma exceção quando os buffers têm comprimentos diferentes. Em frameworks como Express, Fastify ou NestJS, preserve o corpo bruto. Se o middleware de JSON consumir ou transformar o body antes da validação, a assinatura calculada não será a mesma. Por que não usar uma comparação comum? Pode parecer suficiente escrever expected === signatureHeader . Mas comparações comuns podem encerrar o trabalho assim que encontram o primeiro caractere diferente. Em determinados cenários, a variação no tempo de resposta pode revelar informações sobre a assinatura esperada. hash_equals() e timingSafeEqual() foram projetadas para reduzir esse risco por meio de comparação em tempo constante. A assinatura é apenas a primeira camada Uma assinatura válida prova que o payload foi assinado com o secret compartilhado. Ela não transforma todo evento em uma ação autorizada para o seu negócio. Idempotência Armazene o header X-GitHub-Delivery . Se a mesma entrega chegar novamente, responda de maneira idempotente em vez de repetir efeitos colaterais. Eventos permitidos Leia X-GitHub-Event e aceite somente os eventos necessários. Um endpoint criado para push não deve processar qualquer tipo de entrega. Limites e filas Limite o tamanho do body, responda rapidamente e envie trabalho pesado para uma fila. O endpoint do webhook não deve executar todo o processamento de forma síncrona. Logs e retenção Nunca registre o secret. Evite armazenar payloads completos sem necessidade e defina uma política de retenção para dados que possam conter informações sensíveis. Autorização de negócio Mesmo depois de validar o GitHub, confirme se aquele repositório, organização, instalação ou evento pode executar a operação solicitada. Testando a implementação O GitHub fornece um vetor público de teste: secret: It's a Secret to Everybody payload: Hello, World! assinatura esperada: sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17 Use esse valor em testes automatizados e acrescente casos para assinatura correta, assinatura ausente, prefixo inválido, tamanho incorreto, payload modificado e secret vazio. O repositório do guia inclui exemplos independentes de framework e testes executáveis para PHP e Node.js. Fluxo recomendado em produção - Receber o POST via HTTPS. - Preservar o corpo bruto. - Validar a assinatura. - Verificar duplicidade por X-GitHub-Delivery . - Filtrar o tipo em X-GitHub-Event . - Persistir um envelope mínimo. - Enviar o processamento para uma fila. - Responder rapidamente ao GitHub. A validação HMAC é pequena em quantidade de código, mas define a fronteira de confiança de todo o sistema. Se seu produto recebe webhooks, não trate o endpoint como uma simples rota de entrada. Trate-o como uma interface pública exposta a dados não confiáveis. Como você trata idempotência e reprocessamento de webhooks nos seus projetos? Top comments (0)
Comments
No comments yet. Start the discussion.