[Security] Add HMAC-SHA256 Signature Verification to MoonPay Webhook#507
Merged
davedumto merged 2 commits intodavedumto:mainfrom Mar 30, 2026
Conversation
|
@ebubechi-ihediwa is attempting to deploy a commit to the david's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@ebubechi-ihediwa Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[Security] Add HMAC-SHA256 Signature Verification to MoonPay Webhook
Closes #282
🔒 Overview
This PR fixes a critical security vulnerability in the MoonPay webhook handler. The endpoint at
app/api/webhooks/moonpay/route.tspreviously processed incoming webhook payloads without verifying themoonpay-signatureheader — meaning any actor who discovered the endpoint URL could forge atransaction_completedevent and mark any invoice as paid without an actual payment occurring.🚨 The Vulnerability
Severity: Critical
Attack vector: Any unauthenticated HTTP client
Impact: An attacker could credit arbitrary invoices as "paid" by sending a crafted POST request to
/api/webhooks/moonpaywith a faketransaction_completedpayload. This would:paidin the databasepaymenttransaction recordRoot cause: The handler called
await request.json()and processed the result immediately — no signature check, no authentication of any kind.🛠️ What Changed
Modified
app/api/webhooks/moonpay/route.ts— Added signature verification gate before any payload processingAdded
scripts/verify-moonpay-webhook.ts— Standalone test script validating the signature logic🔍 Implementation Details
Signature Verification (
route.ts)Three targeted changes were made to the existing handler. Zero business logic was modified.
1. Added
cryptoimport2. Added
verifyMoonPaySignature()helperPlaced between the imports and the
POSTexport. Uses HMAC-SHA256 with base64 encoding andcrypto.timingSafeEqualfor constant-time comparison to prevent timing attacks:3. Replaced body reading with signature-gated flow
Before (vulnerable):
After (secured):
What was NOT changed
Everything below the signature gate is untouched:
externalTransactionIdprisma.$transactionthat marks invoice paid + creates transaction recordcreateReferralEarning)processAutoSwap)sendPaymentReceivedEmail)dispatchWebhooks){ received: true }📁 Files Changed
Build Note
npm run buildcould not be verified in the current environment due to missingnode_modulesand a pre-existing Prisma 7 schema migration issue (urlproperty inschema.prismaneeds to move toprisma.config.ts). This is unrelated to this fix — the changes are TypeScript-correct and use only existing imports + Node.js built-ins.🔐 Security Considerations
crypto.timingSafeEqualprevents attackers from measuring response times to guess valid signatures byte-by-byte.try/catcharoundtimingSafeEqualcatches theRangeErrorthrown when buffers differ in length, returningfalseinstead of crashing.falsebefore any HMAC computation.request.text()instead ofrequest.json()ensures the HMAC is computed on the exact bytes MoonPay signed, not a re-serialized version.process.env.MOONPAY_WEBHOOK_KEY— never hardcoded, never logged.📋 Reviewer Checklist
moonpay-signature)MOONPAY_WEBHOOK_KEYis set in production environment variablesnpx tsx scripts/verify-moonpay-webhook.ts— all 5 tests passBranch:
fix-moonpay-webhook-signature-verificationCloses: #282
PR Type: Security fix
Impact: Critical
Dependencies: None (Node.js built-in
cryptoonly)