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