From 034a0171ee3b3f749ff0e80eb13dab1c604ddf68 Mon Sep 17 00:00:00 2001 From: Gabriel Moraes Date: Thu, 2 Jul 2026 12:01:43 -0300 Subject: [PATCH] =?UTF-8?q?feat(webhook):=20verifica=C3=A7=C3=A3o=20de=20f?= =?UTF-8?q?rescor=20opcional=20(anti-replay)=20em=20verifySignature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Novo parâmetro opcional `?int $toleranceSeconds`: após validar o HMAC, rejeita entregas cujo `sentAt` (no corpo assinado) esteja fora da janela. Aditivo e retrocompatível (sem o param, comportamento idêntico). Pareado com o backend, que agora assina `id`+`sentAt`; deduplique por `id` para idempotência. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Webhook.php | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/Webhook.php b/src/Webhook.php index 84356fe..d56318e 100644 --- a/src/Webhook.php +++ b/src/Webhook.php @@ -26,15 +26,42 @@ final class Webhook * @param string $rawBody Corpo bruto EXATO da requisição (não re-serialize o JSON). * @param string|null $signatureHeader Valor do header `X-SMSGo-Signature`. * @param string $secret Segredo do webhook (retornado por `setWebhook`/`getWebhook`). + * @param int|null $toleranceSeconds Se informado, além da assinatura exige que o + * `sentAt` do corpo esteja dentro desta janela (anti-replay). Sem ele, o + * comportamento é idêntico ao anterior (só assinatura). Deduplicar pelo `id` + * do corpo continua a cargo do receptor. */ - public static function verifySignature(string $rawBody, ?string $signatureHeader, string $secret): bool - { + public static function verifySignature( + string $rawBody, + ?string $signatureHeader, + string $secret, + ?int $toleranceSeconds = null + ): bool { if ($signatureHeader === null || $signatureHeader === '') { return false; } $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret); - return hash_equals($expected, $signatureHeader); + if (!hash_equals($expected, $signatureHeader)) { + return false; + } + + if ($toleranceSeconds !== null) { + $payload = json_decode($rawBody, true); + $sentAt = is_array($payload) && isset($payload['sentAt']) ? $payload['sentAt'] : null; + if (!is_string($sentAt)) { + return false; + } + $ts = strtotime($sentAt); + if ($ts === false) { + return false; + } + if (abs(time() - $ts) > $toleranceSeconds) { + return false; + } + } + + return true; } }