Offerta di lancio: −50 % sul tuo primo pagamento, fino al 30 novembre.Vedi l'offerta →
webhook·toolkit

GitHub webhook signature mismatch (X-Hub-Signature-256)

GitHub signs deliveries when the webhook has a secret. The X-Hub-Signature-256 header holds sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with that secret. A mismatch almost always comes from verifying something other than the raw body.

Open the GitHub signature validator →

The check, done right

Compute the HMAC over the bytes you received, prefix it with sha256=, and compare with a constant-time function. Do not compare with ===: it leaks timing information.

Node.js
import crypto from 'node:crypto';

function verifyGithub(rawBody, signature, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(signature || '');
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Python
import hmac, hashlib

def verify_github(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or '')

Cause 1: the body was parsed, then re-serialised

Same trap as with every provider: if a JSON middleware ran first, JSON.stringify(req.body) is not what GitHub sent. Read the raw body on the webhook route (express.raw(), await req.text(), request.body in Django).

Cause 2: the content type is application/x-www-form-urlencoded

A GitHub webhook can be set to send application/x-www-form-urlencoded. The body is then payload= followed by the URL-encoded JSON, and the signature covers that whole form body, not the decoded JSON. Either switch the webhook to application/json in the repository settings, or verify the raw form body before decoding it.

Cause 3: the old SHA-1 header or the wrong secret

X-Hub-Signature (without -256) is the legacy SHA-1 signature. Compare X-Hub-Signature-256 with a SHA-256 HMAC. The secret is the one typed in the webhook settings (Settings → Webhooks → Edit); a GitHub App has its own webhook secret in the app settings.

Domande frequenti

Does GitHub sign the request if I leave the secret empty?

No. Without a secret there is no signature header to verify. Set a secret and store it in an environment variable.

How can I see the exact body GitHub sent?

Point the webhook at a Webhook Toolkit URL: the inspector keeps the raw body and headers, and Recent Deliveries in GitHub lets you redeliver the same event.

Other webhook guides

Offerta di lancio · −50 %

Stai facendo il debug di un'integrazione webhook questa settimana?

Il Pass 7 giorni sblocca le spiegazioni AI e il codice dell'handler pronto da incollare, 25 URL permanenti, 30 giorni di cronologia e il tunnel Relay verso localhost. Un solo pagamento, nessun abbonamento.

5 € 2,50 € una volta
Pagamento unico · nessun abbonamento · rimborso entro 14 giorni · prezzo di lancio fino al 30 novembre
GitHub webhook X-Hub-Signature-256 mismatch — how to verify it correctly