Webhook endpoints validate payloads
Why it matters
A webhook endpoint that processes events without signature verification accepts requests from anyone — any attacker who knows your webhook URL can POST a fabricated payment_succeeded or subscription_canceled event and trigger the corresponding business logic (CWE-345: insufficient verification of data authenticity, OWASP A08:2021). Webhook URLs are not secret: they appear in Stripe dashboards, server logs, and network traces. The signature secret is the only real credential protecting these endpoints.
Severity rationale
Medium because exploitation requires knowing the webhook URL, which is less trivially discoverable than a public endpoint, but the impact of a forged event — fraudulent order fulfillment, account privilege escalation — is severe.
Remediation
Verify the webhook signature as the very first operation in your handler, before parsing or acting on the payload. For Stripe in Next.js App Router, disable automatic body parsing and read raw bytes:
// app/api/webhooks/stripe/route.ts
export const dynamic = 'force-dynamic'
export async function POST(req: Request) {
const body = await req.text()
const sig = req.headers.get('stripe-signature')!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
} catch {
return new Response('Invalid signature', { status: 400 })
}
// Only reaches here if signature verified
switch (event.type) { ... }
}
For other providers, use their official SDK verification method — never hand-roll HMAC comparison.
Detection
- ID:
webhook-validates-payloads - Severity:
medium - What to look for: Enumerate all webhook receiver endpoints (routes that receive callbacks from Stripe, GitHub, Clerk, Twilio, SendGrid, or other external services). For each webhook endpoint, check: (1) Is the webhook signature verified before processing? (2) Is raw body available for signature verification (JSON parsing before signature check breaks it)? (3) Is the verification step the very first thing in the handler, before any business logic runs?
- Pass criteria: At least 100% of webhook endpoints verify the signature of incoming payloads using the service's SDK or documented verification method (e.g.,
stripe.webhooks.constructEvent(rawBody, sig, secret),svix.verify(), etc.) before executing any business logic. - Fail criteria: Any webhook endpoint that processes the payload before or without signature verification; or that verifies an already-parsed JSON body (which breaks HMAC verification since the raw bytes matter).
- Skip (N/A) when: No webhook receiver endpoints exist. Signal: no Stripe
webhookshandling, no webhook route files, no references to webhook signature headers in any route handler. - Detail on fail: Example:
POST /api/webhooks/stripe processes events without signature verification. Name the unverified webhook endpoints (e.g., "POST /api/webhooks/stripe processes events without calling stripe.webhooks.constructEvent(); raw body not preserved"). Max 500 chars. - Remediation: Always verify webhook signatures in
app/api/webhooks/route handlers before trusting incoming payload data. For Stripe: disable Next.js body parsing for the webhook route (export const config = { api: { bodyParser: false } }), read the raw body, then callstripe.webhooks.constructEvent(rawBody, request.headers.get('stripe-signature'), process.env.STRIPE_WEBHOOK_SECRET). If this throws, return a 400 immediately without processing. For other services, use their official SDK verification methods. Never process a webhook payload based solely on the URL it arrived at — any attacker can POST to your webhook URL.
External references
- cwe · CWE-345 — Insufficient Verification of Data Authenticity
- cwe · CWE-20 — Improper Input Validation
- owasp:2021 · A08 — Software and Data Integrity Failures
Taxons
History
- 2026-04-18·v1.0.0·Initial import from saas-api-design·automated