diff --git a/docs/WEBHOOK_SECRET.md b/docs/WEBHOOK_SECRET.md new file mode 100644 index 00000000..b8929bc4 --- /dev/null +++ b/docs/WEBHOOK_SECRET.md @@ -0,0 +1,38 @@ +# Webhook Secrets + +## How does the signing work? + +Pi.Alert will use the configured secret to create a hash signature of the request body. This SHA256-HMAC signature will appear in the `X-Webhook-Signature` header of each request to the webhook target URL. You can use the value of this header to validate the request was sent by Pi.Alert. + +## Activating webhook signatures + +All you need to do in order to add a signature to the request headers is to set the `WEBHOOK_SECRET` config value to a non-empty string. + +## Validating webhook deliveries + +There are a few things to keep in mind when validating the webhook delivery: + +- Pi.Alert uses an HMAC hex digest to compute the hash +- The signature in the `X-Webhook-Signature` header always starts with `sha256=` +- The hash signature is generated using the configured `WEBHOOK_SECRET` and the request body. +- Never use a plain `==` operator. Instead, consider using a method like [`secure_compare`](https://www.rubydoc.info/gems/rack/Rack%2FUtils:secure_compare) or [`crypto.timingSafeEqual`](https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b), which performs a "constant time" string comparison to help mitigate certain timing attacks against regular equality operators, or regular loops in JIT-optimized languages. + +## Testing the webhook payload validation + +You can use the following secret and payload to verify that your implementation is working correctly. + +`secret`: 'this is my secret' + +`payload`: '{"test":"this is a test body"}' + +If your implementation is correct, the signature you generated should match the following: + +`signature`: bed21fcc34f98e94fd71c7edb75e51a544b4a3b38b069ebaaeb19bf4be8147e9 + +`X-Webhook-Signature`: sha256=bed21fcc34f98e94fd71c7edb75e51a544b4a3b38b069ebaaeb19bf4be8147e9 + +## More information + +If you want to learn more about webhook security, take a look at [GitHub's webhook documentation](https://docs.github.com/en/webhooks/about-webhooks). + +You can find examples for validating a webhook delivery [here](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries#examples). diff --git a/front/php/templates/language/en_us.json b/front/php/templates/language/en_us.json index bd50ac87..fd5bc4e2 100755 --- a/front/php/templates/language/en_us.json +++ b/front/php/templates/language/en_us.json @@ -533,6 +533,8 @@ "WEBHOOK_REQUEST_METHOD_description" : "The HTTP request method to be used for the webhook call.", "WEBHOOK_SIZE_name" : "Max payload size", "WEBHOOK_SIZE_description" : "The maximum size of the webhook payload as number of characters in the passed string. If above limit, it will be truncated and a (text was truncated) message is appended.", + "WEBHOOK_SECRET_name": "HMAC Secret", + "WEBHOOK_SECRET_description": "When set, use this secret to generate the SHA256-HMAC hex digest value of the request body, which will be passed as the X-Webhook-Signature header to the request. You can find more informations here.", "Apprise_display_name" : "Apprise", "Apprise_icon" : "", "REPORT_APPRISE_name" : "Enable Apprise", @@ -661,4 +663,4 @@ "Donations_Platforms" : "Sponsor platforms", "Donations_Others" : "Others" } -} +} \ No newline at end of file diff --git a/pialert/conf.py b/pialert/conf.py index 9d5ddd30..266579cd 100755 --- a/pialert/conf.py +++ b/pialert/conf.py @@ -68,6 +68,7 @@ REPORT_WEBHOOK = False WEBHOOK_URL = '' WEBHOOK_PAYLOAD = 'json' WEBHOOK_REQUEST_METHOD = 'GET' +WEBHOOK_SECRET = '' # Apprise REPORT_APPRISE = False diff --git a/pialert/initialise.py b/pialert/initialise.py index e2e7f9b3..233bdc0c 100755 --- a/pialert/initialise.py +++ b/pialert/initialise.py @@ -132,6 +132,7 @@ def importConfigs (db): conf.WEBHOOK_PAYLOAD = ccd('WEBHOOK_PAYLOAD', 'json' , c_d, 'Payload type', 'text.select', "['json', 'html', 'text']", 'Webhooks') conf.WEBHOOK_REQUEST_METHOD = ccd('WEBHOOK_REQUEST_METHOD', 'GET' , c_d, 'Req type', 'text.select', "['GET', 'POST', 'PUT']", 'Webhooks') conf.WEBHOOK_SIZE = ccd('WEBHOOK_SIZE', 1024 , c_d, 'Payload size', 'integer', '', 'Webhooks') + conf.WEBHOOK_SECRET = ccd('WEBHOOK_SECRET', '' , c_d, 'Secret', 'text', '', 'Webhooks') # Apprise conf.REPORT_APPRISE = ccd('REPORT_APPRISE', False , c_d, 'Enable Apprise', 'boolean', '', 'Apprise', ['test']) diff --git a/pialert/publishers/webhook.py b/pialert/publishers/webhook.py index c37100e6..d530ac7a 100755 --- a/pialert/publishers/webhook.py +++ b/pialert/publishers/webhook.py @@ -1,5 +1,7 @@ import json import subprocess +import hashlib +import hmac import conf from const import logPath @@ -71,6 +73,7 @@ def send (msg: noti_struc): }] } + # DEBUG - Write the json payload into a log file for debugging write_file (logPath + '/webhook_payload.json', json.dumps(_json_payload)) @@ -81,7 +84,13 @@ def send (msg: noti_struc): curlParams = ["curl","-i","-H", "Content-Type:application/json" ,"-d", json.dumps(_json_payload), _WEBHOOK_URL] else: _WEBHOOK_URL = conf.WEBHOOK_URL - curlParams = ["curl","-i","-X", conf.WEBHOOK_REQUEST_METHOD ,"-H", "Content-Type:application/json" ,"-d", json.dumps(_json_payload), _WEBHOOK_URL] + curlParams = ["curl","-i","-X", conf.WEBHOOK_REQUEST_METHOD , "-H", "Content-Type:application/json", "-d", json.dumps(_json_payload), _WEBHOOK_URL] + + # Add HMAC signature if configured + if(conf.WEBHOOK_SECRET != ''): + h = hmac.new(conf.WEBHOOK_SECRET.encode("UTF-8"), json.dumps(_json_payload, separators=(',', ':')).encode(), hashlib.sha256).hexdigest() + curlParams.insert(4,"-H") + curlParams.insert(5,f"X-Webhook-Signature: sha256={h}") try: # Execute CURL call