Lanceringsaanbieding: −50 % op je eerste betaling, tot 30 november.Bekijk de aanbieding →
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.

Veelgestelde vragen

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

Lanceringsaanbieding · −50 %

Ben je deze week een webhook-integratie aan het debuggen?

De 7-dagen-Pass ontgrendelt AI-uitleg en kant-en-klare handlercode, 25 permanente URL's, 30 dagen geschiedenis en de Relay-tunnel naar localhost. Eén keer betalen, geen abonnement.

€ 5 € 2,50 eenmalig
Eenmalige betaling · geen abonnement · 14 dagen geld terug · lanceringsprijs tot 30 november
GitHub webhook X-Hub-Signature-256 mismatch — how to verify it correctly