From 630a585178ab2421edb20f609257f6c2d06334ab Mon Sep 17 00:00:00 2001 From: Replit Agent Date: Fri, 20 Mar 2026 21:55:04 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20webhook=20HMAC=20=E2=80=94=20Gitea=20sen?= =?UTF-8?q?ds=20raw=20hex,=20not=20sha256=3D=20prefixed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gitea's X-Gitea-Signature header contains raw hex HMAC-SHA256. GitHub's X-Hub-Signature-256 uses the sha256= prefix. verifySignature now normalises both formats to raw hex before timingSafeEqual comparison, so pushes from Gitea trigger deploys. --- vps/webhook.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/vps/webhook.js b/vps/webhook.js index 95d7430..016e6a9 100644 --- a/vps/webhook.js +++ b/vps/webhook.js @@ -43,13 +43,17 @@ function log(msg) { function verifySignature(payload, signature) { const hmac = crypto.createHmac('sha256', SECRET); hmac.update(payload); - const expected = 'sha256=' + hmac.digest('hex'); - // Both buffers must be the same length for timingSafeEqual - if (Buffer.byteLength(expected) !== Buffer.byteLength(signature)) return false; + const hexDigest = hmac.digest('hex'); + // Gitea sends raw hex in X-Gitea-Signature; GitHub/others send sha256= + // Normalise both sides to raw hex before comparing + const incomingHex = signature.startsWith('sha256=') + ? signature.slice(7) + : signature; + if (!incomingHex || Buffer.byteLength(hexDigest) !== Buffer.byteLength(incomingHex)) return false; try { return crypto.timingSafeEqual( - Buffer.from(expected, 'utf8'), - Buffer.from(signature, 'utf8') + Buffer.from(hexDigest, 'utf8'), + Buffer.from(incomingHex, 'utf8') ); } catch (_) { return false;