Skip to content

fix: encrypt webhook secrets before storage in MongoDB (#3956)#3961

Open
atul-upadhyay-7 wants to merge 10 commits into
Premshaw23:masterfrom
atul-upadhyay-7:fix/webhook-secrets-plaintext-storage
Open

fix: encrypt webhook secrets before storage in MongoDB (#3956)#3961
atul-upadhyay-7 wants to merge 10 commits into
Premshaw23:masterfrom
atul-upadhyay-7:fix/webhook-secrets-plaintext-storage

Conversation

@atul-upadhyay-7

Copy link
Copy Markdown
Contributor

Summary

This PR fixes the vulnerability where webhook signing secrets were stored in plaintext in MongoDB. Secrets are now encrypted before storage and never exposed in API responses after creation.

Changes

New: lib/webhook/crypto.js

  • AES-256-GCM encryption for storing secrets in the database
  • HMAC-SHA256 hashing for secret verification
  • Timing-safe comparison to prevent timing attacks
  • Encryption key derived from WEBHOOK_ENCRYPTION_KEY or CRON_SECRET env var

Updated: lib/models/webhookModel.js

  • Secrets are now encrypted (encryptedSecret) and hashed (secretHash) before storage
  • serializeWebhook() strips secret, secretHash, and encryptedSecret from all responses
  • New getWebhookSecretForSigning() function retrieves decrypted secrets for the dispatcher
  • updateWebhook() re-encrypts secrets when updated

Updated: lib/webhook/dispatcher.js

  • Uses getWebhookSecretForSigning() to retrieve decrypted secrets for signing
  • Skips webhooks with decryption failures (logs error)

Updated: app/api/webhooks/route.js

  • POST response now includes the raw secret with a warning: "Save this secret securely. It will not be shown again."
  • GET/list responses never include secrets

Security Model

Operation Before After
Storage Plaintext secret field Encrypted encryptedSecret + hashed secretHash
GET/List Secret exposed in response Secret stripped from response
POST Secret exposed in response Secret returned once with save warning
Signing Dispatcher reads plaintext from DB Dispatcher decrypts from DB

Environment Setup

Set WEBHOOK_ENCRYPTION_KEY in production (falls back to CRON_SECRET):

WEBHOOK_ENCRYPTION_KEY=your-256-bit-secret-here

Testing

  1. Create a webhook via POST /api/webhooks — raw secret returned once in response
  2. List webhooks via GET /api/webhooks — no secret in response
  3. Get webhook by ID — no secret in response
  4. Trigger a webhook event — signature is correctly computed using decrypted secret

Related Issue

Fixes #3956

github-actions Bot and others added 10 commits June 24, 2026 06:10
- Add webhook crypto utility with AES-256-GCM encryption and HMAC-SHA256 hashing
- Encrypt webhook secrets before storing in database
- Strip secrets from all API responses (GET, list)
- Return raw secret only once at creation time with save warning
- Add getWebhookSecretForSigning function for dispatcher to retrieve decrypted secrets
- Update dispatcher to use decrypted secrets for signing

Fixes Premshaw23#3956
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Webhook Signing Secrets Stored in Plaintext in MongoDB

1 participant