Added secret handling for webhooks (#13980)

closes: https://github.com/TryGhost/Team/issues/1203
refs: https://github.com/TryGhost/Ghost/issues/9942

- Ensures that the webhook secret is validated and saved in Ghost admin
- Then makes use of this value by optionally adding an X-Ghost-Signature header that effectively signs the webhooks
- This allows for verifying the source of a webhook coming from Ghost is truly Ghost.
- Uses the same pattern as GitHub uses: https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks

Co-authored-by: Hannah Wolfe <github.erisds@gmail.com>
This commit is contained in:
Georg Grauberger 2022-08-23 18:34:32 +03:00 committed by GitHub
parent c3fb0ef578
commit 36d9ae36ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 25 additions and 7 deletions

View File

@ -954,3 +954,4 @@ add|ember-template-lint|no-passed-in-event-handlers|176|44|176|44|dcb4785647a508
add|ember-template-lint|no-passed-in-event-handlers|186|44|186|44|70487c008d7dda453fef82f0140699ee93c0055c|1660521600000|1670893200000|1676077200000|app/components/modal-tier.hbs
add|ember-template-lint|style-concatenation|303|54|303|54|23293f0c3838b23432d2b2daaf04b34504896d91|1660608000000|1670979600000|1676163600000|app/components/modal-tier.hbs
remove|ember-template-lint|no-nested-interactive|23|28|23|28|5cf783a5684dda036706ff7438472e99c60a88e7|1658102400000|1668474000000|1673658000000|app/templates/whatsnew.hbs
add|ember-template-lint|no-passed-in-event-handlers|72|20|72|20|e639a281bd34eefbe403ddf46501154b89a1f477|1661212800000|1671584400000|1676768400000|app/components/modal-webhook-form.hbs

View File

@ -69,7 +69,7 @@
<label for="webhook-secret" class="fw6">Secret</label>
<GhTextInput
@value={{readonly this.webhook.secret}}
@oninput={{action (mut this.webhook.secret) value="target.value"}}
@input={{action (mut this.webhook.secret) value="target.value"}}
@focus-out={{action "validate" "secret" target=this.webhook}}
@id="webhook-secret"
@name="secret"

View File

@ -3,7 +3,7 @@ import validator from 'validator';
import {isBlank} from '@ember/utils';
export default BaseValidator.create({
properties: ['name', 'event', 'targetUrl'],
properties: ['name', 'event', 'targetUrl', 'secret'],
name(model) {
if (isBlank(model.name)) {
@ -39,5 +39,13 @@ export default BaseValidator.create({
if (model.errors.has('targetUrl')) {
this.invalidate();
}
},
secret(model) {
if (!isBlank(model.secret) && !validator.isLength(model.secret, 0, 191)) {
model.errors.add('secret', 'Secret is too long, max 191 chars');
model.hasValidated.pushObject('secret');
this.invalidate();
}
}
});

View File

@ -1,6 +1,7 @@
const debug = require('@tryghost/debug')('services:webhooks:trigger');
const logging = require('@tryghost/logging');
const ghostVersion = require('@tryghost/version');
const crypto = require('crypto');
class WebhookTrigger {
/**
@ -85,13 +86,21 @@ class WebhookTrigger {
const reqPayload = JSON.stringify(hookPayload);
const url = webhook.get('target_url');
const secret = webhook.get('secret') || '';
const headers = {
'Content-Length': Buffer.byteLength(reqPayload),
'Content-Type': 'application/json',
'Content-Version': `v${ghostVersion.safe}`
};
if (secret !== '') {
headers['X-Ghost-Signature'] = `sha256=${crypto.createHmac('sha256', secret).update(reqPayload).digest('hex')}, t=${Date.now()}`;
}
const opts = {
body: reqPayload,
headers: {
'Content-Length': Buffer.byteLength(reqPayload),
'Content-Type': 'application/json',
'Content-Version': `v${ghostVersion.safe}`
},
headers,
timeout: 2 * 1000,
retry: 5
};