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.
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.
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.
saas-api-design.api-security.webhook-validates-payloadsmediumstripe.webhooks.constructEvent(rawBody, sig, secret), svix.verify(), etc.) before executing any business logic.webhooks handling, no webhook route files, no references to webhook signature headers in any route handler.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.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 call stripe.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.