The risk of unverified webhooks
When integrating with Paystack, Flutterwave, or Stripe, the payment flow is asynchronous. The user completes the transaction on the provider's page, and the provider sends an HTTP POST request (the webhook) to your server to confirm the status.
If your server processes that payload without cryptographically verifying its origin, anyone who discovers the endpoint URL can send a POST request formatted to look like a successful payment. This bypasses the actual payment gateway entirely.
Signature Forgery
Attackers send payloads to your webhook endpoint without the correct HMAC signature. If your application doesn't validate the signature, the fake payment is processed.
Replay Attacks
An attacker intercepts a legitimate, signed webhook and resends it multiple times. If your system lacks idempotency, it credits the user repeatedly.
Timing Attacks
Attackers analyze the response time of your signature verification logic to guess the expected signature byte by byte. Use constant-time string comparison to prevent this.
Mandatory mitigation: HMAC signature verification
Every major payment provider signs their webhook payloads using an HMAC (Hash-based Message Authentication Code). They generate a hash using the payload body and a shared secret key, placing the result in a custom HTTP header (e.g., `x-paystack-signature` or `stripe-signature`).
- Capture the raw body: Do not parse the JSON before verifying the signature. The HMAC is calculated on the raw, stringified payload. Any formatting changes will invalidate the hash.
- Calculate your own hash: Use your environment's secret key and the raw payload to generate an HMAC-SHA256 (or provider-specified) hash.
- Use constant-time comparison: Compare the header signature with your calculated hash using a secure compare function (like `crypto.timingSafeEqual` in Node.js) to prevent timing attacks.
Are your payment webhooks vulnerable to forgery or replay attacks?
Get an API Security AuditDefending against replay attacks
Even with perfect signature validation, a valid webhook can be intercepted and resent. To prevent a single ₦10,000 payment from crediting a wallet ten times, you must implement idempotency.
Use the unique transaction reference or event ID provided in the webhook payload as an idempotency key. Before processing the event, check your database or a distributed cache (like Redis). If the key has already been processed, respond with a 200 OK but skip the business logic.
How we test webhook security
During an API security assessment, our engineers actively attempt to break webhook integrations using several techniques:
- Sending payloads with missing signature headers.
- Sending payloads with invalid signatures or mismatched algorithms.
- Modifying the payload body (e.g., changing the amount) while keeping a valid signature from a previous request.
- Replaying legitimate webhook payloads.
- Testing for Server-Side Request Forgery (SSRF) if the webhook URL is dynamically constructed or if the application fetches remote resources based on the payload.
Verification as a middleware, not a suggestion
A common vulnerability pattern we see in Nigerian fintechs is placing the signature verification logic inside the controller, where it occasionally gets bypassed by new routes or refactoring. Signature validation should be implemented as a strict middleware that drops invalid requests before they ever reach your business logic.
Related reading
Blog: The Most Dangerous API Vulnerability · Fintech API Security: 10 Steps
Services: API Security
Industries: Payment Gateways
Frequently asked questions
{faq.q}
{faq.a}