Webhook signature mismatch - secret and body checked

Issue Summary
Signature mismatch on webhook

Steps to Reproduce

  1. Create a new mock server on postman. E.g. https://8e11246e-2511-4b8d-a236-bb6e90712ccd.mock.pstmn.io
  2. Create a new webhook on Ghost, with secret = “test123” and event “post.published”
  3. Publish a new post from the Ghost interface
  4. Check the logs from the postman mockup server to retrieve the calculated signature and the json body
  5. Run node.js code to recalculate signature with the secret and verify they are mismatched

Setup information

Ghost Version
Hosted on ghost.org

Relevant log / error output
Example log from Postman
x-ghost-signature: sha256=6d30fe32b3128f5370936aa10e0fc3b9fe4e9b0ebf428bc9378f4f24f501f20c, t=1732539451946"
body:
{"post":{"current":{"id":"67447438e61fb200012f48e0","uuid":"001cdb2b-98bf-4b05-b46c-15b3b93cade0","title":"test","slug":"test","mobiledoc":null,"html":"<p>test</p>","comment_id":"67447438e61fb200012f48e0","plaintext":"test","feature_image":null,"featured":false,"status":"published","visibility":"public","created_at":"2024-11-25T12:57:28.000Z","updated_at":"2024-11-25T12:57:31.000Z","published_at":"2024-11-25T12:57:31.000Z","custom_excerpt":null,"codeinjection_head":null,"codeinjection_foot":null,"custom_template":null,"canonical_url":null,"authors":[{"id":"1","name":"Lnk.Bio","slug":"lnk","email":"info@lnk.bio","profile_image":"https://www.gravatar.com/avatar/36927dbfe7c60a2fe221af9e66d42c60?s=250&r=x&d=mp","cover_image":null,"bio":null,"website":null,"location":null,"facebook":null,"twitter":null,"accessibility":"{\"onboarding\":{\"completedSteps\":[],\"checklistState\":\"started\"}}","status":"active","meta_title":null,"meta_description":null,"tour":null,"last_seen":"2024-11-25T12:04:11.000Z","comment_notifications":true,"free_member_signup_notification":true,"paid_subscription_started_notification":true,"paid_subscription_canceled_notification":false,"mention_notifications":true,"recommendation_notifications":true,"milestone_notifications":true,"donation_notifications":true,"created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T12:04:12.000Z","roles":[{"id":"6743f1c2e207cd00088541fd","name":"Owner","description":"Blog Owner","created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T03:40:50.000Z"}],"url":"https://lnk-bio.ghost.io/author/lnk/"}],"tags":[],"post_revisions":[{"id":"67447438e61fb200012f48e2","post_id":"67447438e61fb200012f48e0","lexical":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"t\",\"type\":\"extended-text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","created_at_ts":1732539448357,"created_at":"2024-11-25T12:57:28.000Z","title":"test","post_status":"draft","reason":"initial_revision","feature_image":null,"feature_image_alt":null,"feature_image_caption":null,"custom_excerpt":null,"author":{"id":"1","name":"Lnk.Bio","slug":"lnk","email":"info@lnk.bio","profile_image":"https://www.gravatar.com/avatar/36927dbfe7c60a2fe221af9e66d42c60?s=250&r=x&d=mp","cover_image":null,"bio":null,"website":null,"location":null,"facebook":null,"twitter":null,"accessibility":"{\"onboarding\":{\"completedSteps\":[],\"checklistState\":\"started\"}}","status":"active","locale":null,"visibility":"public","meta_title":null,"meta_description":null,"tour":null,"last_seen":"2024-11-25T12:04:11.000Z","comment_notifications":true,"free_member_signup_notification":true,"paid_subscription_started_notification":true,"paid_subscription_canceled_notification":false,"mention_notifications":true,"recommendation_notifications":true,"milestone_notifications":true,"donation_notifications":true,"created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T12:04:12.000Z"}},{"id":"6744743be61fb200012f48e5","post_id":"67447438e61fb200012f48e0","lexical":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"test\",\"type\":\"extended-text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","created_at_ts":1732539451878,"created_at":"2024-11-25T12:57:31.000Z","title":"test","post_status":"published","reason":"published","feature_image":null,"feature_image_alt":null,"feature_image_caption":null,"custom_excerpt":null}],"tiers":[{"id":"6743f1c2e207cd0008854206","name":"Free","slug":"free","active":true,"welcome_page_url":null,"visibility":"public","trial_days":0,"description":null,"type":"free","currency":null,"monthly_price":null,"yearly_price":null,"created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T03:40:50.000Z","monthly_price_id":null,"yearly_price_id":null},{"id":"6743f1c2e207cd0008854207","name":"Lnk.Bio","slug":"default-product","active":true,"welcome_page_url":null,"visibility":"public","trial_days":0,"description":null,"type":"paid","currency":"usd","monthly_price":500,"yearly_price":5000,"created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T03:40:50.000Z","monthly_price_id":null,"yearly_price_id":null}],"count":{"clicks":0,"positive_feedback":0,"negative_feedback":0},"primary_author":{"id":"1","name":"Lnk.Bio","slug":"lnk","email":"info@lnk.bio","profile_image":"https://www.gravatar.com/avatar/36927dbfe7c60a2fe221af9e66d42c60?s=250&r=x&d=mp","cover_image":null,"bio":null,"website":null,"location":null,"facebook":null,"twitter":null,"accessibility":"{\"onboarding\":{\"completedSteps\":[],\"checklistState\":\"started\"}}","status":"active","meta_title":null,"meta_description":null,"tour":null,"last_seen":"2024-11-25T12:04:11.000Z","comment_notifications":true,"free_member_signup_notification":true,"paid_subscription_started_notification":true,"paid_subscription_canceled_notification":false,"mention_notifications":true,"recommendation_notifications":true,"milestone_notifications":true,"donation_notifications":true,"created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T12:04:12.000Z","roles":[{"id":"6743f1c2e207cd00088541fd","name":"Owner","description":"Blog Owner","created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T03:40:50.000Z"}],"url":"https://lnk-bio.ghost.io/author/lnk/"},"primary_tag":null,"email_segment":"all","url":"https://lnk-bio.ghost.io/test/","excerpt":"test","reading_time":0,"og_image":null,"og_title":null,"og_description":null,"twitter_image":null,"twitter_title":null,"twitter_description":null,"meta_title":null,"meta_description":null,"email_subject":null,"frontmatter":null,"feature_image_alt":null,"feature_image_caption":null,"email_only":false},"previous":{"status":"draft","updated_at":"2024-11-25T12:57:28.000Z","html":"<p>t</p>","plaintext":"test","published_at":null,"tiers":[{"id":"6743f1c2e207cd0008854206","name":"Free","slug":"free","active":true,"welcome_page_url":null,"visibility":"public","trial_days":0,"description":null,"type":"free","currency":null,"monthly_price":null,"yearly_price":null,"created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T03:40:50.000Z","monthly_price_id":null,"yearly_price_id":null},{"id":"6743f1c2e207cd0008854207","name":"Lnk.Bio","slug":"default-product","active":true,"welcome_page_url":null,"visibility":"public","trial_days":0,"description":null,"type":"paid","currency":"usd","monthly_price":500,"yearly_price":5000,"created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T03:40:50.000Z","monthly_price_id":null,"yearly_price_id":null}]}}}

example signature recalculation
const crypto = require(‘crypto’);
const secret = ‘test123’;
const payload = ‘’ // the post from above
signature = crypto.createHmac(‘sha256’, secret).update(payload, ‘utf8’).digest(‘hex’);
console.log(Signature: ${signature});
generated signature:
37e4b1fc2ee4e8a64242607628f988ba0c7524c45f318799f5a131c8b76f7132

So the difference is between
37e4b1fc2ee4e8a64242607628f988ba0c7524c45f318799f5a131c8b76f7132 (recalculated)
6d30fe32b3128f5370936aa10e0fc3b9fe4e9b0ebf428bc9378f4f24f501f20c (from header)

I don’t see any logic issue in how the sha256 is generated and the body is the exact one provided by ghost.

See also setup image from the webhook panel

You’re missing the timestamp from the body, here are a couple of related threads:

And here’s how I implemented verification a couple weeks ago:

2 Likes

Thank you very much, not sure how we missed this change. Also tried searching before posting.

Thanks for flagging this.

2 Likes