Securing SaaS Webhooks: Protecting Stripe & Paddle Endpoints
For modern software-as-a-service (SaaS) applications, webhooks are the lifeblood of integration. They process critical event notifications from payment providers like Stripe and Paddle, alerting your application when a subscription is created, a payment succeeds, or a customer cancels. However, because webhook endpoints are public URLs, they are highly exposed to security threats. If they are not properly secured, an attacker can forge webhook events and gain free access to your paid features. Let's explore how to protect your webhook endpoints.
The Threat: Webhook Spoofing
Since a webhook endpoint is just a public API route on your server (e.g., `/api/webhooks/stripe`), anyone on the internet can send POST requests to it. If your endpoint simply trusts the payload it receives and updates user subscription states in your database without verification, an attacker can easily construct a faked `checkout.session.completed` payload, send it to your server, and activate premium accounts for free.
How Cryptographic Signature Verification Works
To prevent spoofing, reputable providers like Stripe and Paddle sign the request payload cryptographically before sending it. When configuring a webhook, the provider shares a secret key (known as the Webhook Signing Secret) with you. Here is the process:
- 1.The provider computes a HMAC-SHA256 signature using the raw payload body and the shared secret.
- 2.The provider sends this signature in the request headers (e.g., Stripe-Signature for Stripe, or X-Signature for Paddle).
- 3.Your server receives the request, reads the raw body, and computes its own HMAC signature using the same shared secret.
- 4.If the signatures match, the payload is authentic. If they don't, the request is rejected immediately.
Common Webhook Security Pitfalls
Even when developers attempt to implement verification, they often fall into common traps:
- • Using Parsed Bodies: Many frameworks automatically parse JSON requests (producing `req.body`). Calculating signatures on parsed JSON is unreliable because whitespace, key ordering, and escaping can change, leading to signature mismatches or bypasses. Always use the raw buffer of the request body.
- • Neglecting Timestamp Checks: In addition to signatures, Stripe sends a timestamp header. This helps prevent replay attacks, where an attacker intercepts a legitimate webhook and resends it repeatedly. Ensure your verification checks that the signature's timestamp is within a reasonable window (e.g., under 5 minutes).
- • Hardcoding Secrets: Keep your signing secrets in environment variables (`STRIPE_WEBHOOK_SECRET`) and rotate them periodically.
Implementation: Stripe Webhook in Next.js
Here is a secure implementation of a Stripe webhook endpoint in Next.js App Router. Note the usage of `request.text()` to read the raw body, and standard SDK verification:
Automated Webhook Auditing with CodeSec
Manual audits are prone to error, especially when developers temporarily disable signature verification for testing and forget to re-enable it. CodeSec makes this verification foolproof.
CodeSec's Webhook Verifier automatically discovers your webhook endpoints, sends them mock Stripe and Paddle payloads with invalid/missing signatures, and alerts you if your server returns a successful response. This ensures that you are protected against payment bypass vulnerabilities and signature spoofing attacks before your code reaches production.