c285b0a0f1
refs [ENG-1238](https://linear.app/tryghost/issue/ENG-1238/🔒-webhook-signatures-dont-include-timestamp-in-the-signature) Added timestamp to the webhook signature hash to prevent replay attacks. This is a breaking change for webhook consumers as signature verification logic will need to be updated to account for the timestamp in the hash, for example: ```js const crypto = require('crypto'); // Webhook secret from Ghost Admin const WEBHOOK_SECRET = 'FOOBARBAZ' // Sample incoming webhook request object const req = { headers: { 'x-ghost-signature': 'sha256=fc9749d5b3333109bd779f65d4b1b891576bc5c92febea3b1d186a7f946d0745, t=1719842984367' }, body: { tag: { current: { id: '6682b8a8e10cc04306284330', name: 'test', slug: 'test', description: null, feature_image: null, visibility: 'public', og_image: null, og_title: null, og_description: null, twitter_image: null, twitter_title: null, twitter_description: null, meta_title: null, meta_description: null, codeinjection_head: null, codeinjection_foot: null, canonical_url: null, accent_color: null, created_at: '2024-07-01T14:09:44.000Z', updated_at: '2024-07-01T14:09:44.000Z', url: 'http://localhost:2368/404/' }, previous: {} } } }; // Get the request body as a JSON string const reqBodyJSON = JSON.stringify(req.body); // Extract the hash and timestamp from the x-ghost-signature header const {sha256: hash, t: timestamp} = req.headers['x-ghost-signature'] .split(', ') .map((x) => x.split('=')) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) // Recreate the hash using the secret, request body, and timestamp and compare it to the hash from the header const isValid = crypto.createHmac('sha256', WEBHOOK_SECRET).update(`${reqBodyJSON}${timestamp}`).digest('hex') === hash if (isValid) { console.log('Valid signature!') } ``` |
||
---|---|---|
.. | ||
e2e-api | ||
e2e-browser | ||
e2e-frontend | ||
e2e-server | ||
e2e-webhooks | ||
integration | ||
regression | ||
unit | ||
utils | ||
.eslintignore | ||
.eslintrc.js |