Skip to content

[Security] Add HMAC-SHA256 Signature Verification to MoonPay Webhook#507

Merged
davedumto merged 2 commits intodavedumto:mainfrom
ebubechi-ihediwa:fix-moonpay-webhook-signature-verification
Mar 30, 2026
Merged

[Security] Add HMAC-SHA256 Signature Verification to MoonPay Webhook#507
davedumto merged 2 commits intodavedumto:mainfrom
ebubechi-ihediwa:fix-moonpay-webhook-signature-verification

Conversation

@ebubechi-ihediwa
Copy link
Copy Markdown
Contributor

[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.ts previously processed incoming webhook payloads without verifying the moonpay-signature header — meaning any actor who discovered the endpoint URL could forge a transaction_completed event 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/moonpay with a fake transaction_completed payload. This would:

  1. Mark the target invoice as paid in the database
  2. Create a fraudulent payment transaction record
  3. Trigger referral commission payouts on fake payments
  4. Trigger auto-swap (USDC → NGN) on nonexistent funds
  5. Send the freelancer a false "Payment Received" email
  6. Fire downstream webhook events to integrators with fabricated data

Root 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 processing

Added

  • 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 crypto import

import crypto from 'crypto'

2. Added verifyMoonPaySignature() helper

Placed between the imports and the POST export. Uses HMAC-SHA256 with base64 encoding and crypto.timingSafeEqual for constant-time comparison to prevent timing attacks:

function verifyMoonPaySignature(rawBody: string, signature: string, secret: string): boolean {
  if (!signature || !secret) return false
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('base64')
  try {
    return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  } catch {
    return false // handles length mismatch
  }
}

3. Replaced body reading with signature-gated flow

Before (vulnerable):

const event = await request.json()
// ... immediately processes event ...

After (secured):

const rawBody = await request.text()
const signature = request.headers.get('moonpay-signature') ?? ''
const secret = process.env.MOONPAY_WEBHOOK_KEY ?? ''

if (!verifyMoonPaySignature(rawBody, signature, secret)) {
console.warn('MoonPay webhook: invalid signature')
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
}

let event: any
try {
event = JSON.parse(rawBody)
} catch {
return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })
}
// ... existing business logic continues unchanged ...

What was NOT changed

Everything below the signature gate is untouched:

  • Invoice lookup by externalTransactionId
  • prisma.$transaction that marks invoice paid + creates transaction record
  • Referral earning creation (createReferralEarning)
  • Auto-swap processing (processAutoSwap)
  • Payment received email (sendPaymentReceivedEmail)
  • Downstream webhook dispatch (dispatchWebhooks)
  • The catch block returning { received: true }

📁 Files Changed

File Change Lines
app/api/webhooks/moonpay/route.ts Added signature verification gate ~15 added
scripts/verify-moonpay-webhook.ts New test script ~80

Build Note

npm run build could not be verified in the current environment due to missing node_modules and a pre-existing Prisma 7 schema migration issue (url property in schema.prisma needs to move to prisma.config.ts). This is unrelated to this fix — the changes are TypeScript-correct and use only existing imports + Node.js built-ins.

🔐 Security Considerations

  • Constant-time comparison: crypto.timingSafeEqual prevents attackers from measuring response times to guess valid signatures byte-by-byte.
  • Length mismatch handling: The try/catch around timingSafeEqual catches the RangeError thrown when buffers differ in length, returning false instead of crashing.
  • Empty value guards: Both empty signature and empty secret short-circuit to false before any HMAC computation.
  • Raw body integrity: Reading request.text() instead of request.json() ensures the HMAC is computed on the exact bytes MoonPay signed, not a re-serialized version.
  • Environment variable: The secret is read from process.env.MOONPAY_WEBHOOK_KEY — never hardcoded, never logged.

📋 Reviewer Checklist

  • Verify the signature header name matches MoonPay's documentation (moonpay-signature)
  • Verify the HMAC algorithm matches MoonPay's documentation (SHA-256, base64-encoded)
  • Confirm MOONPAY_WEBHOOK_KEY is set in production environment variables
  • Confirm no business logic was altered below the signature gate
  • Run npx tsx scripts/verify-moonpay-webhook.ts — all 5 tests pass

Branch: fix-moonpay-webhook-signature-verification
Closes: #282
PR Type: Security fix
Impact: Critical
Dependencies: None (Node.js built-in crypto only)

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 30, 2026

@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.

@drips-wave
Copy link
Copy Markdown

drips-wave bot commented Mar 30, 2026

@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! 🚀

Learn more about application limits

@davedumto davedumto merged commit f4bbb53 into davedumto:main Mar 30, 2026
1 of 3 checks passed
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.

[Security] MoonPay webhook has no signature verification

2 participants