{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/python/verify-webhook-origin/",
  "description": "Secure webhooks by verifying payload authenticity using HMAC hash signatures with shared secrets, preventing man-in-the-middle attacks.",
  "path": "/python/verify-webhook-origin/",
  "publishedAt": "2022-09-18T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Python",
    "API",
    "Security"
  ],
  "textContent": "While working with GitHub webhooks, I discovered a common [webhook security pattern] a\nreceiver can adopt to verify that the incoming webhooks are indeed arriving from GitHub; not\nfrom some miscreant trying to carry out a man-in-the-middle attack. After some amount of\ndigging, I found that it's quite a common practice that many other webhook services employ\nas well. Also, check out how [Sentry handles webhook verification].\n\nMoreover, GitHub's documentation demonstrates the pattern in Ruby. So I thought it'd be a\ngood idea to translate that into Python in a more platform-agnostic manner. The core idea of\nthe pattern goes as follows:\n\n- The webhook sender will hash the JSONified webhook payload with a well-known hashing\n  algorithm like MD5, SHA-1, or SHA-256. A secret token known to the receiver will be used\n  to sign the calculated hash of the payload.\n\n- The sender will include the payload hash digest prefixed by the name of the hash algorithm\n  to the header of the webhook request. For example, the GitHub webhook's request header has\n  a key like the following. Notice how the digest is prefixed with the name of the algorithm\n  sha256:\n\n    \n\n- The webhook receiver is then expected to hash the received JSON payload with the same\n  algorithm found in the prefix of the header and sign with the common secret token known to\n  both the sender and the receiver. Afterward, the receiver compares the calculated hash\n  with the incoming hash in the request header. If the two digests match, that ensures that\n  the payload hasn't been tampered with. Otherwise, the receiver should reject the incoming\n  payload. This provides a second layer of protection over the usual authentication that the\n  receiver might have in place.\n\nTo demonstrate the workflow, here's an example of how the webhook sender might be\nimplemented:\n\nHere, I've implemented a simple POST API that:\n\n- Accepts a payload from the user.\n- Hashes the payload with sha-256 algorithm and signs it with a some-secret token.\n- Adds the digest to the request header to the receiver. The header has a key called\n  X-Payload-Signature-256 that contains the prefixed payload digest:\n\n    \n\n- After hashing, the sender sends the payload to the receiver via HTTP POST request. Here,\n  I'm using HTTPx to send the request to the receiver. For demonstration purposes, I'm\n  assuming that the receiver endpoint is localhost:6000/receive-webhook.\n\nThe receiver will:\n\n- Accept the incoming request from the sender.\n- Parse the header and store the value of X-Payload-Signature-256.\n- Calculate the hash value of the incoming payload in the same manner as the sender.\n- Sign the payload with the common secret that's known to both parties.\n- Compare the newly calculated signed-hash with the digest value of the\n  X-Payload-Signature-256 attribute.\n- Only accept and process the payload if the incoming and the computed hashes match.\n\nHere's how you can implement the receiver:\n\n> In the receiver, instead of using plain string comparison to compare the payload hashes,\n> leverage secrets.compare_digest to mitigate the possibility of [timing attacks].\n\nTo test the end-to-end workflow, you'll need to pip install [httpx] and [uvicorn]. Then on\nyour console, you can run the two scripts in the background with the following command:\n\nThis will spin up two uvicorn servers in the background where the sender and the receiver\ncan be accessed via ports 5000 and 6000 respectively. Now if you make a request to the\nsender service, you'll see that the sender sends the webhook payload to the receiver service\nand returns an HTTP 200 code only if the receiver has been able to verify the signed-hash of\nthe payload:\n\nThis will return:\n\nThe reciver will return a HTTP 400 error code if it can't verify the payload. Once you're\ndone, kill the running servers with sudo pkill uvicorn command.\n\n\n\n\n[webhook security pattern]:\n    https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks\n\n[sentry handles webhook verification]:\n    https://docs.sentry.io/product/integrations/integration-platform/webhooks/#sentry-hook-resource\n\n[timing attacks]:\n    https://en.wikipedia.org/wiki/Timing_attack\n\n[httpx]:\n    https://www.python-httpx.org/\n\n[uvicorn]:\n    https://www.uvicorn.org/",
  "title": "Verifying webhook origin via payload hash signing"
}